Files

1272 lines
37 KiB
Go

package console
/*
Sliver Implant Framework
Copyright (C) 2019 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"bufio"
"context"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/bishopfox/sliver/client/assets"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/client/spin"
"github.com/bishopfox/sliver/client/theme"
"github.com/bishopfox/sliver/client/version"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/util"
"github.com/gofrs/uuid"
"github.com/kballard/go-shellquote"
"github.com/reeflective/console"
"github.com/reeflective/readline"
"github.com/spf13/cobra"
"golang.org/x/exp/slog"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
)
const (
defaultTimeout = 60
)
const (
// Terminal control sequences (not "styling").
Clearln = "\r\x1b[2K"
UpN = "\033[%dA"
DownN = "\033[%dB"
)
// Observer - A function to call when the sessions changes.
type (
Observer func(*clientpb.Session, *clientpb.Beacon)
BeaconTaskCallback func(*clientpb.BeaconTask)
)
type SliverClient struct {
App *console.Console
Rpc rpcpb.SliverRPCClient
ActiveTarget *ActiveTarget
EventListeners *sync.Map
BeaconTaskCallbacks map[string]BeaconTaskCallback
BeaconTaskCallbacksMutex *sync.Mutex
Settings *assets.ClientSettings
IsServer bool
IsCLI bool
serverCmds console.Commands
sliverCmds console.Commands
jsonHandler slog.Handler
printf func(format string, args ...any) (int, error)
stdoutPipeWriter *os.File
stdoutPipeDone chan struct{}
stdoutPipeOnce sync.Once
stdoutSyncMu sync.Mutex
stdoutSyncSeq uint64
stdoutSyncMarker []byte
stdoutSyncAcks map[uint64]chan struct{}
stdoutSyncAcksMu sync.Mutex
connMu sync.Mutex
grpcConn *grpc.ClientConn
backgroundRPC rpcpb.SliverRPCClient
backgroundConn *grpc.ClientConn
backgroundDedicated bool
connDetails *ConnectionDetails
connCancel context.CancelFunc
connWg *sync.WaitGroup
connectionHooksApplied bool
logCommandHookApplied bool
suppressedEventNotifs atomic.Int32
// These writers are always safe to use (never error); the underlying stream
// can be swapped on server switch to avoid breaking io.MultiWriter/io.Copy.
jsonRemoteWriter *optionalRemoteWriter
asciicastRemoteWriter *optionalRemoteWriter
jsonRemoteStream rpcpb.SliverRPC_ClientLogClient
asciicastRemoteStream rpcpb.SliverRPC_ClientLogClient
}
// NewConsole creates the sliver client (and console), creating menus and prompts.
// The returned console does neither have commands nor a working RPC connection yet,
// thus has not started monitoring any server events, or started the application.
func NewConsole(isServer bool) *SliverClient {
assets.Setup(false, false)
_ = theme.LoadAndSetCurrentTheme()
ApplyTheme(theme.Current())
settings, _ := assets.LoadSettings()
con := &SliverClient{
App: console.New("sliver"),
ActiveTarget: &ActiveTarget{
observers: map[int]Observer{},
observerID: 0,
},
EventListeners: &sync.Map{},
BeaconTaskCallbacks: map[string]BeaconTaskCallback{},
BeaconTaskCallbacksMutex: &sync.Mutex{},
IsServer: isServer,
Settings: settings,
connWg: &sync.WaitGroup{},
}
// Ensure logging never panics even if console logs are disabled.
con.jsonHandler = slog.NewJSONHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelDebug})
// The active target needs access to the console
// to automatically switch between command menus.
con.ActiveTarget.con = con
// Readline-shell (edition) settings
if settings.VimMode {
con.App.Shell().Config.Set("editing-mode", "vi")
}
// Global console settings
con.App.NewlineBefore = true
con.App.NewlineAfter = true
// Server menu.
server := con.App.Menu(consts.ServerMenu)
server.Short = "Server commands"
server.Prompt().Primary = con.GetPrompt
server.AddInterrupt(readline.ErrInterrupt, con.exitConsole) // Ctrl-C
server.AddHistorySourceFile("server history", filepath.Join(assets.GetRootAppDir(), "history"))
// Implant menu.
sliver := con.App.NewMenu(consts.ImplantMenu)
sliver.Short = "Implant commands"
sliver.Prompt().Primary = con.GetPrompt
sliver.AddInterrupt(io.EOF, con.exitImplantMenu) // Ctrl-D
con.App.SetPrintLogo(func(_ *console.Console) {
con.PrintLogo()
})
return con
}
// Init requires a working RPC connection to the sliver server, and 2 different sets of commands.
// If run is true, the console application is started, making this call blocking. Otherwise, commands and
// RPC connection are bound to the console (making the console ready to run), but the console does not start.
func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, grpcConn *grpc.ClientConn, details *ConnectionDetails, serverCmds, sliverCmds console.Commands, run bool, rcScript string) error {
con.IsCLI = !run
con.serverCmds = serverCmds
con.sliverCmds = sliverCmds
// The console application needs to query the terminal for cursor positions
// when asynchronously printing logs (that is, when no command is running).
// If ran from a system shell, however, those queries will block because
// the system shell is in control of stdin. So just use the classic Printf.
if con.IsCLI {
con.printf = fmt.Printf
} else {
con.printf = con.App.TransientPrintf
}
// Bind commands to the app
server := con.App.Menu(consts.ServerMenu)
server.SetCommands(serverCmds)
sliver := con.App.Menu(consts.ImplantMenu)
sliver.SetCommands(sliverCmds)
con.applyConnectionHooksOnce()
// console logger
if con.Settings.ConsoleLogs {
// Classic logs
consoleLog := getConsoleLogFile()
con.ensureJSONRemoteWriter()
con.setupLogger(consoleLog, con.jsonRemoteWriter)
defer consoleLog.Close()
// Ascii cast sessions (complete terminal interface) are only useful
// for the interactive console. In CLI mode they would clobber stdout.
if !con.IsCLI {
asciicastLog := getConsoleAsciicastFile()
defer asciicastLog.Close()
con.ensureAsciicastRemoteWriter()
con.setupAsciicastRecord(asciicastLog, con.asciicastRemoteWriter)
}
}
if err := con.SetConnection(rpc, grpcConn, details); err != nil {
return err
}
if rcScript != "" {
originalPrintf := con.printf
con.printf = fmt.Printf
con.runRCScript(serverCmds, sliverCmds, rcScript)
con.printf = originalPrintf
}
if !con.IsCLI {
return con.App.Start()
}
return nil
}
func (con *SliverClient) runRCScript(serverCmds, sliverCmds console.Commands, rcScript string) {
scanner := bufio.NewScanner(strings.NewReader(rcScript))
lineNumber := 0
for scanner.Scan() {
lineNumber++
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
if err := con.runRCLine(serverCmds, sliverCmds, line); err != nil {
con.PrintErrorf("rc line %d error: %s", lineNumber, err)
}
}
if err := scanner.Err(); err != nil {
con.PrintErrorf("rc script error: %s", err)
}
}
func (con *SliverClient) runRCLine(serverCmds, sliverCmds console.Commands, line string) error {
args, err := shellquote.Split(line)
if err != nil {
return fmt.Errorf("parse error: %w", err)
}
if con.serverCmds == nil {
con.serverCmds = serverCmds
}
if con.sliverCmds == nil {
con.sliverCmds = sliverCmds
}
menu := con.App.ActiveMenu()
if menu != nil {
// Reset per line to avoid stale roots when rc scripts switch menus.
menu.Command = nil
}
for _, hook := range con.App.PreCmdRunLineHooks {
args, err = hook(args)
if err != nil {
return fmt.Errorf("pre-run line error: %w", err)
}
}
if len(args) == 0 {
return nil
}
menu = con.App.ActiveMenu()
if menu == nil {
return fmt.Errorf("no active menu")
}
if menu.Command == nil {
con.setMenuCommand(menu, args)
}
if menu.Command == nil {
return fmt.Errorf("no commands available")
}
target, _, _ := menu.Command.Find(args)
if err := menu.CheckIsAvailable(target); err != nil {
return err
}
for _, hook := range con.App.PreCmdRunHooks {
if err := hook(); err != nil {
return fmt.Errorf("pre-run error: %w", err)
}
}
menu.SetArgs(args)
menu.SetContext(context.Background())
if err := menu.Execute(); err != nil {
return err
}
return nil
}
func (con *SliverClient) applyConnectionHooksOnce() {
con.connMu.Lock()
defer con.connMu.Unlock()
if con.connectionHooksApplied {
return
}
con.connectionHooksApplied = true
con.App.PreReadlineHooks = append(con.App.PreReadlineHooks, con.syncOutputHook)
con.App.PostCmdRunHooks = append(con.App.PostCmdRunHooks, con.syncOutputHook)
con.App.PreCmdRunLineHooks = append(con.App.PreCmdRunLineHooks, con.refreshDedicatedCommandConnectionHook)
con.App.PreCmdRunLineHooks = append(con.App.PreCmdRunLineHooks, con.allowServerRootCommands)
if shell := con.App.Shell(); shell != nil && shell.Completer != nil {
baseCompleter := shell.Completer
shell.Completer = func(line []rune, cursor int) readline.Completions {
con.prepareCompletion(line, cursor)
return baseCompleter(line, cursor)
}
}
}
func (con *SliverClient) startEventLoop(ctx context.Context, rpc rpcpb.SliverRPCClient) {
if rpc == nil {
return
}
eventStream, err := rpc.Events(ctx, &commonpb.Empty{})
if err != nil {
fmt.Printf("%s%s\n", Warn, err)
return
}
for {
event, err := eventStream.Recv()
if err == io.EOF || event == nil {
return
}
go con.triggerEventListeners(event)
// Trigger event based on type
switch event.EventType {
case consts.CanaryEvent:
sessions := con.GetSessionsByName(event.Session.Name)
if con.eventNotificationsSuppressed() {
affected := make([]string, 0, len(sessions))
for _, session := range sessions {
affected = append(affected, strings.Split(session.ID, "-")[0])
}
message := fmt.Sprintf("%s %s has been burned (DNS Canary)", StyleBold.Render("WARNING:"), event.Session.Name)
if len(affected) > 0 {
message += " Affected sessions: " + strings.Join(affected, ", ")
}
con.emitToastEvent("error", message)
break
}
con.PrintEventErrorf("%s %s has been burned (DNS Canary)", StyleBold.Render("WARNING:"), event.Session.Name)
for _, session := range sessions {
shortID := strings.Split(session.ID, "-")[0]
con.PrintErrorf("\t🔥 Session %s is affected", shortID)
}
case consts.WatchtowerEvent:
msg := string(event.Data)
sessions := con.GetSessionsByName(event.Session.Name)
if con.eventNotificationsSuppressed() {
affected := make([]string, 0, len(sessions))
for _, session := range sessions {
affected = append(affected, strings.Split(session.ID, "-")[0])
}
message := fmt.Sprintf("%s %s has been burned (seen on %s)", StyleBold.Render("WARNING:"), event.Session.Name, msg)
if len(affected) > 0 {
message += " Affected sessions: " + strings.Join(affected, ", ")
}
con.emitToastEvent("error", message)
break
}
con.PrintEventErrorf("%s %s has been burned (seen on %s)", StyleBold.Render("WARNING:"), event.Session.Name, msg)
for _, session := range sessions {
shortID := strings.Split(session.ID, "-")[0]
con.PrintErrorf("\t🔥 Session %s is affected", shortID)
}
case consts.JoinedEvent:
if con.Settings.UserConnect {
con.emitConsoleNotification("info", false, "%s has joined the game", event.Client.Operator.Name)
}
case consts.LeftEvent:
if con.Settings.UserConnect {
con.emitConsoleNotification("info", false, "%s left the game", event.Client.Operator.Name)
}
case consts.JobStoppedEvent:
job := event.Job
con.emitConsoleNotification("error", false, "Job #%d stopped (%s/%s)", job.ID, job.Protocol, job.Name)
case consts.SessionOpenedEvent:
session := event.Session
currentTime := time.Now().Format(time.RFC1123)
shortID := strings.Split(session.ID, "-")[0]
con.emitConsoleNotification("info", true, "Session %s %s - %s (%s) - %s/%s - %v",
shortID, session.Name, session.RemoteAddress, session.Hostname, session.OS, session.Arch, currentTime)
case consts.SessionUpdateEvent:
session := event.Session
currentTime := time.Now().Format(time.RFC1123)
shortID := strings.Split(session.ID, "-")[0]
con.emitConsoleNotification("info", false, "Session %s has been updated - %v", shortID, currentTime)
case consts.SessionClosedEvent:
session := event.Session
currentTime := time.Now().Format(time.RFC1123)
shortID := strings.Split(session.ID, "-")[0]
con.emitConsoleNotification("error", true, "Lost session %s %s - %s (%s) - %s/%s - %v",
shortID, session.Name, session.RemoteAddress, session.Hostname, session.OS, session.Arch, currentTime)
activeSession := con.ActiveTarget.GetSession()
core.GetTunnels().CloseForSession(session.ID)
core.CloseCursedProcesses(session.ID)
if activeSession != nil && activeSession.ID == session.ID {
con.ActiveTarget.Set(nil, nil)
con.emitConsoleNotification("error", false, "Active session disconnected")
}
case consts.BeaconRegisteredEvent:
beacon := &clientpb.Beacon{}
proto.Unmarshal(event.Data, beacon)
currentTime := time.Now().Format(time.RFC1123)
shortID := strings.Split(beacon.ID, "-")[0]
con.emitConsoleNotification("info", true, "Beacon %s %s - %s (%s) - %s/%s - %v",
shortID, beacon.Name, beacon.RemoteAddress, beacon.Hostname, beacon.OS, beacon.Arch, currentTime)
case consts.BeaconTaskResultEvent:
con.triggerBeaconTaskCallback(event.Data)
}
con.triggerReactions(event)
}
}
// CreateEventListener - creates a new event listener and returns its ID.
func (con *SliverClient) CreateEventListener() (string, <-chan *clientpb.Event) {
listener := make(chan *clientpb.Event, 100)
listenerID, _ := uuid.NewV4()
con.EventListeners.Store(listenerID.String(), listener)
return listenerID.String(), listener
}
// SuppressEventNotifications redirects console event messages to local listeners instead of stdout.
func (con *SliverClient) SuppressEventNotifications() func() {
if con == nil {
return func() {}
}
con.suppressedEventNotifs.Add(1)
return func() {
if con == nil {
return
}
if con.suppressedEventNotifs.Add(-1) < 0 {
con.suppressedEventNotifs.Store(0)
}
}
}
func (con *SliverClient) eventNotificationsSuppressed() bool {
return con != nil && con.suppressedEventNotifs.Load() > 0
}
func (con *SliverClient) emitToastEvent(level string, message string) {
if con == nil || con.EventListeners == nil || strings.TrimSpace(message) == "" {
return
}
con.triggerEventListeners(&clientpb.Event{
EventType: consts.ClientToastEvent,
Data: []byte(message),
Err: strings.TrimSpace(level),
})
}
func (con *SliverClient) emitConsoleNotification(level string, emphasized bool, format string, args ...any) {
message := fmt.Sprintf(format, args...)
if con.eventNotificationsSuppressed() {
con.emitToastEvent(level, message)
return
}
switch strings.ToLower(strings.TrimSpace(level)) {
case "error", "warn", "warning":
if emphasized {
con.PrintEventErrorf("%s", message)
} else {
con.PrintErrorf("%s", message)
}
case "success":
if emphasized {
con.PrintEventSuccessf("%s", message)
} else {
con.PrintSuccessf("%s", message)
}
default:
if emphasized {
con.PrintEventInfof("%s", message)
} else {
con.PrintInfof("%s", message)
}
}
}
// RemoveEventListener - removes an event listener given its id.
func (con *SliverClient) RemoveEventListener(listenerID string) {
value, ok := con.EventListeners.LoadAndDelete(listenerID)
if ok {
close(value.(chan *clientpb.Event))
}
}
func (con *SliverClient) triggerEventListeners(event *clientpb.Event) {
con.EventListeners.Range(func(key, value interface{}) bool {
listener := value.(chan *clientpb.Event)
listener <- event // Do not block while sending the event to the listener
return true
})
}
func (con *SliverClient) triggerReactions(event *clientpb.Event) {
reactions := core.Reactions.On(event.EventType)
if len(reactions) == 0 {
return
}
// We need some special handling for SessionOpenedEvent to
// set the new session as the active session
currentActiveSession, currentActiveBeacon := con.ActiveTarget.Get()
defer func() {
con.ActiveTarget.Set(currentActiveSession, currentActiveBeacon)
}()
switch event.EventType {
case consts.SessionOpenedEvent:
con.ActiveTarget.Set(nil, nil)
con.ActiveTarget.Set(event.Session, nil)
case consts.BeaconRegisteredEvent:
con.ActiveTarget.Set(nil, nil)
beacon := &clientpb.Beacon{}
proto.Unmarshal(event.Data, beacon)
con.ActiveTarget.Set(nil, beacon)
}
for _, reaction := range reactions {
for _, line := range reaction.Commands {
con.emitConsoleNotification("info", false, "%s '%s'", StyleBold.Render("Execute reaction:"), line)
err := con.App.ActiveMenu().RunCommandLine(context.Background(), line)
if err != nil {
con.emitConsoleNotification("error", false, "Reaction command error: %s", err)
}
}
}
}
// triggerBeaconTaskCallback - Triggers the callback for a beacon task.
func (con *SliverClient) triggerBeaconTaskCallback(data []byte) {
task := &clientpb.BeaconTask{}
err := proto.Unmarshal(data, task)
if err != nil {
con.PrintErrorf("\rCould not unmarshal beacon task: %s", err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
beacon, _ := con.Rpc.GetBeacon(ctx, &clientpb.Beacon{ID: task.BeaconID})
// If the callback is not in our map then we don't do anything, the beacon task
// was either issued by another operator in multiplayer mode or the client process
// was restarted between the time the task was created and when the server got the result
con.BeaconTaskCallbacksMutex.Lock()
defer con.BeaconTaskCallbacksMutex.Unlock()
if callback, ok := con.BeaconTaskCallbacks[task.ID]; ok {
if con.Settings.BeaconAutoResults {
if beacon != nil {
con.PrintSuccessf("%s completed task %s", beacon.Name, strings.Split(task.ID, "-")[0])
}
task_content, err := con.Rpc.GetBeaconTaskContent(ctx, &clientpb.BeaconTask{
ID: task.ID,
})
con.Printf(Clearln + "\r")
if err == nil {
callback(task_content)
} else {
con.PrintErrorf("Could not get beacon task content: %s", err)
}
}
delete(con.BeaconTaskCallbacks, task.ID)
}
}
func (con *SliverClient) AddBeaconCallback(taskID string, callback BeaconTaskCallback) {
con.BeaconTaskCallbacksMutex.Lock()
defer con.BeaconTaskCallbacksMutex.Unlock()
con.BeaconTaskCallbacks[taskID] = callback
}
func (con *SliverClient) GetPrompt() string {
promptStyle := assets.PromptStyleHost
if con.Settings != nil {
promptStyle = assets.NormalizePromptStyle(con.Settings.PromptStyle)
}
if promptStyle == assets.PromptStyleCustom {
src := assets.DefaultPromptTemplate
if con.Settings != nil && strings.TrimSpace(con.Settings.PromptTemplate) != "" {
src = con.Settings.PromptTemplate
}
out, err := renderPromptTemplate(con, src)
if err != nil {
return Clearln + " > "
}
if strings.TrimSpace(out) == "" {
return Clearln + " > "
}
return Clearln + out
}
prompt := StyleUnderline.Render("sliver")
if con.IsServer {
if promptStyle != assets.PromptStyleBasic {
prompt = StyleBold.Render("[server]") + " " + prompt
}
} else if promptStyle != assets.PromptStyleBasic {
// sliver-client only: optionally include operator/host context.
if details, _, ok := con.CurrentConnection(); ok && details != nil && details.Config != nil {
operator := strings.TrimSpace(details.Config.Operator)
host := strings.TrimSpace(details.Config.LHost)
prefix := ""
switch promptStyle {
case assets.PromptStyleOperatorHost:
if operator != "" && host != "" {
prefix = operator + "@" + host
} else if operator != "" {
prefix = operator
} else if host != "" {
prefix = host
}
case assets.PromptStyleHost:
if host != "" {
prefix = host
}
}
if prefix != "" {
prompt = StyleBoldPrimary.Render("["+prefix+"]") + " " + prompt
}
}
}
if session := con.ActiveTarget.GetSession(); session != nil {
prompt += StyleBoldRed.Render(" (" + session.Name + ")")
} else if beacon := con.ActiveTarget.GetBeacon(); beacon != nil {
prompt += StyleBoldBlue.Render(" (" + beacon.Name + ")")
}
prompt += " > "
return Clearln + prompt
}
func (con *SliverClient) PrintLogo() {
// Always show a logo even if the server connection is transiently unavailable.
logo := asciiLogos[util.Intn(len(asciiLogos))]
fmt.Println(strings.ReplaceAll(logo.Render(), "\n", "\r\n"))
fmt.Println("All hackers gain " + abilities[util.Intn(len(abilities))] + "\r")
if con.Rpc == nil {
fmt.Println(Warn + "Not connected to a server\r")
fmt.Printf("%sClient %s\r\n", Info, version.FullVersion())
fmt.Println(Info + "Welcome to the sliver shell, please type 'help' for options\r")
fmt.Println("")
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
serverVer, err := con.Rpc.GetVersion(ctx, &commonpb.Empty{})
if err != nil {
fmt.Printf("%sCould not query server version: %s\r\n", Warn, err)
fmt.Printf("%sClient %s\r\n", Info, version.FullVersion())
fmt.Println(Info + "Welcome to the sliver shell, please type 'help' for options\r")
fmt.Println("")
con.CheckLastUpdate()
return
}
dirty := ""
if serverVer.Dirty {
dirty = " - " + StyleBold.Render("Dirty")
}
serverSemVer := fmt.Sprintf("%d.%d.%d", serverVer.Major, serverVer.Minor, serverVer.Patch)
fmt.Printf("%sServer v%s - %s%s\r\n", Info, serverSemVer, serverVer.Commit, dirty)
if version.GitCommit != serverVer.Commit {
fmt.Printf("%sClient %s\r\n", Info, version.FullVersion())
}
fmt.Println(Info + "Welcome to the sliver shell, please type 'help' for options\r")
if serverVer.Major != int32(version.SemanticVersion()[0]) {
fmt.Print(Warn + "Warning: Client and server may be running incompatible versions.\r\n")
}
fmt.Println("")
con.CheckLastUpdate()
}
func (con *SliverClient) CheckLastUpdate() {
now := time.Now()
lastUpdate := getLastUpdateCheck()
compiledAt, err := version.Compiled()
if err != nil {
log.Printf("Failed to parse compiled at timestamp %s", err)
return
}
day := 24 * time.Hour
if compiledAt.Add(30 * day).Before(now) {
if lastUpdate == nil || lastUpdate.Add(30*day).Before(now) {
con.Printf("%sCheck for updates with the 'update' command\n\n", Info)
}
}
}
func getLastUpdateCheck() *time.Time {
appDir := assets.GetRootAppDir()
lastUpdateCheckPath := filepath.Join(appDir, consts.LastUpdateCheckFileName)
data, err := os.ReadFile(lastUpdateCheckPath)
if err != nil {
log.Printf("Failed to read last update check %s", err)
return nil
}
unixTime, err := strconv.Atoi(string(data))
if err != nil {
log.Printf("Failed to parse last update check %s", err)
return nil
}
lastUpdate := time.Unix(int64(unixTime), 0)
return &lastUpdate
}
func (con *SliverClient) GetSession(arg string) *clientpb.Session {
sessions, err := con.Rpc.GetSessions(context.Background(), &commonpb.Empty{})
if err != nil {
con.PrintWarnf("%s", err)
return nil
}
for _, session := range sessions.GetSessions() {
if session.Name == arg || strings.HasPrefix(session.ID, arg) {
return session
}
}
return nil
}
// GetSessionsByName - Return all sessions for an Implant by name.
func (con *SliverClient) GetSessionsByName(name string) []*clientpb.Session {
sessions, err := con.Rpc.GetSessions(context.Background(), &commonpb.Empty{})
if err != nil {
fmt.Printf("%s%s\n", Warn, err)
return nil
}
matched := []*clientpb.Session{}
for _, session := range sessions.GetSessions() {
if session.Name == name {
matched = append(matched, session)
}
}
return matched
}
// GetActiveSessionConfig - Get the active sessions's config
// TODO: Switch to query config based on ConfigID.
func (con *SliverClient) GetActiveSessionConfig() *clientpb.ImplantConfig {
session := con.ActiveTarget.GetSession()
if session == nil {
return nil
}
c2s := []*clientpb.ImplantC2{}
c2s = append(c2s, &clientpb.ImplantC2{
URL: session.GetActiveC2(),
Priority: uint32(0),
})
config := &clientpb.ImplantConfig{
ID: session.ID,
GOOS: session.GetOS(),
GOARCH: session.GetArch(),
Debug: true,
Evasion: session.GetEvasion(),
MaxConnectionErrors: uint32(1000),
ReconnectInterval: int64(60),
Format: clientpb.OutputFormat_SHELLCODE,
IsSharedLib: true,
C2: c2s,
}
/* If this config will be used to build an implant,
we need to make sure to include the correct transport
for the build */
switch session.Transport {
case "mtls":
config.IncludeMTLS = true
case "http(s)":
config.IncludeHTTP = true
case "dns":
config.IncludeDNS = true
case "wg":
config.IncludeWG = true
case "namedpipe":
config.IncludeNamePipe = true
case "tcppivot":
config.IncludeTCP = true
}
return config
}
func (con *SliverClient) GetActiveBeaconConfig() *clientpb.ImplantConfig {
beacon := con.ActiveTarget.GetBeacon()
if beacon == nil {
return nil
}
c2s := []*clientpb.ImplantC2{}
c2s = append(c2s, &clientpb.ImplantC2{
URL: beacon.ActiveC2,
Priority: uint32(0),
})
config := &clientpb.ImplantConfig{
ID: beacon.ID,
GOOS: beacon.OS,
GOARCH: beacon.Arch,
Debug: false,
IsBeacon: true,
BeaconInterval: beacon.Interval,
BeaconJitter: beacon.Jitter,
Evasion: beacon.Evasion,
MaxConnectionErrors: uint32(1000),
ReconnectInterval: int64(60),
Format: clientpb.OutputFormat_SHELLCODE,
IsSharedLib: true,
C2: c2s,
}
/* If this config will be used to build an implant,
we need to make sure to include the correct transport
for the build */
switch beacon.Transport {
case "mtls":
config.IncludeMTLS = true
case "http":
config.IncludeHTTP = true
case "dns":
config.IncludeDNS = true
case "wg":
config.IncludeWG = true
case "namedpipe":
config.IncludeNamePipe = true
case "tcppivot":
config.IncludeTCP = true
}
return config
}
// exitConsole prompts the user for confirmation to exit the console.
func (c *SliverClient) exitConsole(_ *console.Console) {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Confirm exit (Y/y, Ctrl-C): ")
text, _ := reader.ReadString('\n')
answer := strings.TrimSpace(text)
if (answer == "Y") || (answer == "y") {
c.FlushOutput()
os.Exit(0)
}
}
// exitImplantMenu uses the background command to detach from the implant menu.
func (c *SliverClient) exitImplantMenu(_ *console.Console) {
root := c.App.Menu(consts.ImplantMenu).Command
root.SetArgs([]string{"background"})
root.Execute()
}
func (con *SliverClient) SpinUntil(message string, ctrl chan bool) {
go spin.Until(os.Stdout, message, ctrl)
}
// FormatDateDelta - Generate formatted date string of the time delta between then and now.
func (con *SliverClient) FormatDateDelta(t time.Time, includeDate bool, color bool) string {
nextTime := t.Format(time.UnixDate)
var interval string
if t.Before(time.Now()) {
if includeDate {
interval = fmt.Sprintf("%s (%s ago)", nextTime, time.Since(t).Round(time.Second))
} else {
interval = time.Since(t).Round(time.Second).String()
}
if color {
interval = StyleBoldRed.Render(interval)
}
} else {
if includeDate {
interval = fmt.Sprintf("%s (in %s)", nextTime, time.Until(t).Round(time.Second))
} else {
interval = time.Until(t).Round(time.Second).String()
}
if color {
interval = StyleBoldGreen.Render(interval)
}
}
return interval
}
// GrpcContext - Generate a context for a GRPC request, if no grumble context or an invalid flag is provided 60 seconds is used instead.
func (con *SliverClient) GrpcContext(cmd *cobra.Command) (context.Context, context.CancelFunc) {
if cmd == nil {
return context.WithTimeout(context.Background(), 60*time.Second)
}
timeOutF, _ := cmd.Flags().GetInt64("timeout")
timeout := time.Duration(int64(time.Second) * timeOutF)
if timeout < 1 {
timeout = 60 * time.Second
}
return context.WithTimeout(context.Background(), timeout)
}
//
// -------------------------- [ Active Target ] --------------------------
//
type ActiveTarget struct {
session *clientpb.Session
beacon *clientpb.Beacon
observers map[int]Observer
observerID int
con *SliverClient
}
// GetSessionInteractive - Get the active target(s).
func (s *ActiveTarget) GetInteractive() (*clientpb.Session, *clientpb.Beacon) {
if s.session == nil && s.beacon == nil {
fmt.Print(Warn + "Please select a session or beacon via `use`\n")
return nil, nil
}
return s.session, s.beacon
}
// GetSessionInteractive - Get the active target(s).
func (s *ActiveTarget) Get() (*clientpb.Session, *clientpb.Beacon) {
return s.session, s.beacon
}
// GetSessionInteractive - GetSessionInteractive the active session.
func (s *ActiveTarget) GetSessionInteractive() *clientpb.Session {
if s.session == nil {
fmt.Print(Warn + "Please select a session via `use`\n")
return nil
}
return s.session
}
// GetSession - Same as GetSession() but doesn't print a warning.
func (s *ActiveTarget) GetSession() *clientpb.Session {
return s.session
}
// GetBeaconInteractive - Get beacon interactive the active session.
func (s *ActiveTarget) GetBeaconInteractive() *clientpb.Beacon {
if s.beacon == nil {
fmt.Print(Warn + "Please select a beacon via `use`\n")
return nil
}
return s.beacon
}
// GetBeacon - Same as GetBeacon() but doesn't print a warning.
func (s *ActiveTarget) GetBeacon() *clientpb.Beacon {
return s.beacon
}
// IsSession - Is the current target a session?
func (s *ActiveTarget) IsSession() bool {
return s.session != nil
}
// IsBeacon - Is the current target a beacon?
func (s *ActiveTarget) IsBeacon() bool {
return s.beacon != nil
}
// AddObserver - Observers to notify when the active session changes
func (s *ActiveTarget) AddObserver(observer Observer) int {
s.observerID++
s.observers[s.observerID] = observer
return s.observerID
}
func (s *ActiveTarget) RemoveObserver(observerID int) {
delete(s.observers, observerID)
}
func (s *ActiveTarget) Request(cmd *cobra.Command) *commonpb.Request {
if s.session == nil && s.beacon == nil {
return nil
}
// One less than the gRPC timeout so that the server should timeout first
timeOutF := int64(defaultTimeout) - 1
if cmd != nil {
timeOutF, _ = cmd.Flags().GetInt64("timeout")
}
timeout := (int64(time.Second) * timeOutF) - 1
req := &commonpb.Request{}
req.Timeout = timeout
if s.session != nil {
req.Async = false
req.SessionID = s.session.ID
}
if s.beacon != nil {
req.Async = true
req.BeaconID = s.beacon.ID
}
return req
}
// Set - Change the active session.
func (s *ActiveTarget) Set(session *clientpb.Session, beacon *clientpb.Beacon) {
if session != nil && beacon != nil {
s.con.PrintErrorf("cannot set both an active beacon and an active session")
return
}
defer s.con.ExposeCommands()
// Backgrounding
if session == nil && beacon == nil {
s.session = nil
s.beacon = nil
for _, observer := range s.observers {
observer(s.session, s.beacon)
}
if s.con.IsCLI {
return
}
// Switch back to server menu.
if s.con.App.ActiveMenu().Name() == consts.ImplantMenu {
s.con.App.SwitchMenu(consts.ServerMenu)
}
return
}
// Foreground
if session != nil {
s.session = session
s.beacon = nil
for _, observer := range s.observers {
observer(s.session, s.beacon)
}
} else if beacon != nil {
s.beacon = beacon
s.session = nil
for _, observer := range s.observers {
observer(s.session, s.beacon)
}
}
if s.con.IsCLI {
return
}
// Update menus, prompts and commands
if s.con.App.ActiveMenu().Name() != consts.ImplantMenu {
s.con.App.SwitchMenu(consts.ImplantMenu)
}
}
// Background - Background the active session.
func (s *ActiveTarget) Background() {
defer s.con.App.ShowCommands()
s.session = nil
s.beacon = nil
for _, observer := range s.observers {
observer(nil, nil)
}
// Switch back to server menu.
if !s.con.IsCLI && s.con.App.ActiveMenu().Name() == consts.ImplantMenu {
s.con.App.SwitchMenu(consts.ServerMenu)
}
}
// GetHostUUID - Get the Host's UUID (ID in the database)
func (s *ActiveTarget) GetHostUUID() string {
if s.IsSession() {
return s.session.UUID
} else if s.IsBeacon() {
return s.beacon.UUID
}
return ""
}
// Expose or hide commands if the active target does support them (or not).
// Ex; hide Windows commands on Linux implants, Wireguard tools on HTTP C2, etc.
func (con *SliverClient) ExposeCommands() {
con.App.ShowCommands()
if con.ActiveTarget.session == nil && con.ActiveTarget.beacon == nil {
return
}
filters := make([]string, 0)
// Target type.
switch {
case con.ActiveTarget.session != nil:
session := con.ActiveTarget.session
filters = append(filters, consts.BeaconCmdsFilter)
// Operating system
if session.OS != "windows" {
filters = append(filters, consts.WindowsCmdsFilter)
}
// C2 stack
if session.Transport != "wg" {
filters = append(filters, consts.WireguardCmdsFilter)
}
case con.ActiveTarget.beacon != nil:
beacon := con.ActiveTarget.beacon
filters = append(filters, consts.SessionCmdsFilter)
// Operating system
if beacon.OS != "windows" {
filters = append(filters, consts.WindowsCmdsFilter)
}
// C2 stack
if beacon.Transport != "wg" {
filters = append(filters, consts.WireguardCmdsFilter)
}
}
// Use all defined filters.
con.App.HideCommands(filters...)
}
var abilities = []string{
"first strike",
"vigilance",
"haste",
"indestructible",
"hexproof",
"deathtouch",
"fear",
"epic",
"ninjitsu",
"recover",
"persist",
"conspire",
"reinforce",
"exalted",
"annihilator",
"infect",
"undying",
"living weapon",
"miracle",
"scavenge",
"cipher",
"evolve",
"dethrone",
"hidden agenda",
"prowess",
"dash",
"exploit",
"renown",
"skulk",
"improvise",
"assist",
"jump-start",
}
type asciiLogoStyle int
const (
logoStyleRed asciiLogoStyle = iota
logoStyleGreen
logoStyleBoldGray
)
type asciiLogo struct {
style asciiLogoStyle
art string
}
func (l asciiLogo) Render() string {
switch l.style {
case logoStyleRed:
return StyleRed.Render(l.art)
case logoStyleGreen:
return StyleGreen.Render(l.art)
case logoStyleBoldGray:
return StyleBoldGray.Render(l.art)
default:
return l.art
}
}
var asciiLogos = []asciiLogo{
{style: logoStyleRed, art: `
██████ ██▓ ██▓ ██▒ █▓▓█████ ██▀███
▒██ ▒ ▓██▒ ▓██▒▓██░ █▒▓█ ▀ ▓██ ▒ ██▒
░ ▓██▄ ▒██░ ▒██▒ ▓██ █▒░▒███ ▓██ ░▄█ ▒
▒ ██▒▒██░ ░██░ ▒██ █░░▒▓█ ▄ ▒██▀▀█▄
▒██████▒▒░██████▒░██░ ▒▀█░ ░▒████▒░██▓ ▒██▒
▒ ▒▓▒ ▒ ░░ ▒░▓ ░░▓ ░ ▐░ ░░ ▒░ ░░ ▒▓ ░▒▓░
░ ░▒ ░ ░░ ░ ▒ ░ ▒ ░ ░ ░░ ░ ░ ░ ░▒ ░ ▒░
░ ░ ░ ░ ░ ▒ ░ ░░ ░ ░░ ░
░ ░ ░ ░ ░ ░ ░ ░
`},
{style: logoStyleGreen, art: `
███████╗██╗ ██╗██╗ ██╗███████╗██████╗
██╔════╝██║ ██║██║ ██║██╔════╝██╔══██╗
███████╗██║ ██║██║ ██║█████╗ ██████╔╝
╚════██║██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗
███████║███████╗██║ ╚████╔╝ ███████╗██║ ██║
╚══════╝╚══════╝╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝
`},
{style: logoStyleBoldGray, art: `
.------..------..------..------..------..------.
|S.--. ||L.--. ||I.--. ||V.--. ||E.--. ||R.--. |
| :/\: || :/\: || (\/) || :(): || (\/) || :(): |
| :\/: || (__) || :\/: || ()() || :\/: || ()() |
| '--'S|| '--'L|| '--'I|| '--'V|| '--'E|| '--'R|
` + "`------'`------'`------'`------'`------'`------'" + `
`},
}