mirror of
https://github.com/BishopFox/sliver
synced 2026-06-08 10:29:05 +00:00
429 lines
11 KiB
Go
429 lines
11 KiB
Go
package shell
|
|
|
|
/*
|
|
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 (
|
|
"context"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/bishopfox/sliver/client/command/settings"
|
|
"github.com/bishopfox/sliver/client/console"
|
|
"github.com/bishopfox/sliver/client/core"
|
|
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
|
"github.com/jedib0t/go-pretty/v6/table"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
const (
|
|
darwin = "darwin"
|
|
linux = "linux"
|
|
)
|
|
|
|
type shellResult int
|
|
|
|
const (
|
|
shellExited shellResult = iota
|
|
shellDetached
|
|
shellAttachFailed
|
|
)
|
|
|
|
// ShellCmd - Start an interactive shell on the remote system.
|
|
func ShellCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
|
session := con.ActiveTarget.GetSessionInteractive()
|
|
if session == nil {
|
|
return
|
|
}
|
|
|
|
if !settings.IsUserAnAdult(con) {
|
|
return
|
|
}
|
|
|
|
shellPath, _ := cmd.Flags().GetString("shell-path")
|
|
noPty, _ := cmd.Flags().GetBool("no-pty")
|
|
if con.ActiveTarget.GetSession().OS != linux && con.ActiveTarget.GetSession().OS != darwin {
|
|
noPty = true // Sliver's PTYs are only supported on linux/darwin
|
|
}
|
|
|
|
result := runInteractive(cmd, shellPath, noPty, con, nil)
|
|
if result == shellDetached {
|
|
con.Println("Shell detached")
|
|
} else if result == shellExited {
|
|
con.Println("Shell exited")
|
|
}
|
|
}
|
|
|
|
// ShellLsCmd lists local shell tunnels currently managed by the client.
|
|
func ShellLsCmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
|
|
managed := shells.List()
|
|
if len(managed) == 0 {
|
|
con.PrintInfof("No managed shells\n")
|
|
return
|
|
}
|
|
|
|
tw := table.NewWriter()
|
|
tw.SetStyle(settings.GetTableStyle(con))
|
|
tw.AppendHeader(table.Row{
|
|
"ID",
|
|
"State",
|
|
"Session",
|
|
"PID",
|
|
"PTY",
|
|
"Tunnel ID",
|
|
"Path",
|
|
"Age",
|
|
})
|
|
|
|
now := time.Now()
|
|
for _, sh := range managed {
|
|
age := now.Sub(sh.CreatedAt).Round(time.Second).String()
|
|
if sh.CreatedAt.IsZero() {
|
|
age = "-"
|
|
}
|
|
pid := "-"
|
|
if sh.Pid > 0 {
|
|
pid = strconv.FormatUint(uint64(sh.Pid), 10)
|
|
}
|
|
|
|
tw.AppendRow(table.Row{
|
|
sh.ID,
|
|
sh.State(),
|
|
sessionLabel(sh.SessionID, sh.SessionName),
|
|
pid,
|
|
sh.EnablePTY,
|
|
sh.TunnelID,
|
|
sh.Path,
|
|
age,
|
|
})
|
|
}
|
|
|
|
con.Printf("%s\n", tw.Render())
|
|
}
|
|
|
|
// ShellAttachCmd re-attaches local stdin/stdout to a managed shell tunnel.
|
|
func ShellAttachCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
|
if len(args) < 1 {
|
|
con.PrintErrorf("Usage: shell attach <id>\n")
|
|
return
|
|
}
|
|
|
|
shellID, err := strconv.Atoi(args[0])
|
|
if err != nil || shellID <= 0 {
|
|
con.PrintErrorf("Invalid shell id: %s\n", args[0])
|
|
return
|
|
}
|
|
|
|
sh, ok := shells.Get(shellID)
|
|
if !ok {
|
|
con.PrintErrorf("Unknown shell id: %d\n", shellID)
|
|
return
|
|
}
|
|
if sh.State() == shellStateAttached {
|
|
con.PrintErrorf("Shell %d is already attached\n", shellID)
|
|
return
|
|
}
|
|
if sh.State() == shellStateClosing {
|
|
con.PrintErrorf("Shell %d is closing\n", shellID)
|
|
return
|
|
}
|
|
tunnel := sh.Tunnel()
|
|
if tunnel == nil || !tunnel.IsOpen() {
|
|
con.PrintErrorf("Shell %d is no longer active\n", shellID)
|
|
shells.Remove(shellID)
|
|
return
|
|
}
|
|
|
|
session := con.ActiveTarget.GetSession()
|
|
if session == nil || session.ID != sh.SessionID {
|
|
con.PrintErrorf("Shell %d belongs to %s; switch targets before attaching\n", shellID, sessionLabel(sh.SessionID, sh.SessionName))
|
|
return
|
|
}
|
|
|
|
result := runInteractive(cmd, sh.Path, !sh.EnablePTY, con, sh)
|
|
if result == shellDetached {
|
|
con.Println("Shell detached")
|
|
} else if result == shellExited {
|
|
con.Println("Shell exited")
|
|
}
|
|
}
|
|
|
|
// ShellKillCmd kills a managed shell from the REPL and cleans up remote state.
|
|
func ShellKillCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
|
if len(args) < 1 {
|
|
con.PrintErrorf("Usage: shell kill <id>\n")
|
|
return
|
|
}
|
|
|
|
shellID, err := strconv.Atoi(args[0])
|
|
if err != nil || shellID <= 0 {
|
|
con.PrintErrorf("Invalid shell id: %s\n", args[0])
|
|
return
|
|
}
|
|
force, _ := cmd.Flags().GetBool("force")
|
|
|
|
sh, ok := shells.Get(shellID)
|
|
if !ok {
|
|
con.PrintErrorf("Unknown shell id: %d\n", shellID)
|
|
return
|
|
}
|
|
|
|
session := con.ActiveTarget.GetSession()
|
|
if session == nil || session.ID != sh.SessionID {
|
|
con.PrintErrorf("Shell %d belongs to %s; switch targets before killing\n", shellID, sessionLabel(sh.SessionID, sh.SessionName))
|
|
return
|
|
}
|
|
|
|
sh.setState(shellStateClosing)
|
|
sh.SetOutput(io.Discard)
|
|
shells.Remove(sh.ID)
|
|
|
|
// Best-effort graceful request before closing the tunnel.
|
|
requestRemoteShellExit(sh.Tunnel())
|
|
|
|
if err := closeShellTunnel(con, sh.TunnelID, sh.SessionID); err != nil {
|
|
con.PrintWarnf("Error closing shell tunnel %d: %s\n", sh.TunnelID, err)
|
|
}
|
|
|
|
if force && sh.Pid > 1 {
|
|
terminate, err := con.Rpc.Terminate(context.Background(), &sliverpb.TerminateReq{
|
|
Request: con.ActiveTarget.Request(cmd),
|
|
Pid: int32(sh.Pid),
|
|
Force: force,
|
|
})
|
|
if err != nil {
|
|
con.PrintWarnf("Error terminating shell process %d: %s\n", sh.Pid, err)
|
|
} else if terminate.Response != nil && terminate.Response.GetErr() != "" {
|
|
errMsg := terminate.Response.GetErr()
|
|
// If shell already exited, that is an acceptable cleanup outcome.
|
|
if strings.Contains(strings.ToLower(errMsg), "no such process") ||
|
|
strings.Contains(strings.ToLower(errMsg), "process not found") {
|
|
con.PrintInfof("Shell process %d already exited\n", sh.Pid)
|
|
} else {
|
|
con.PrintWarnf("Terminate response for shell process %d: %s\n", sh.Pid, errMsg)
|
|
}
|
|
} else {
|
|
con.PrintInfof("Shell process %d terminated\n", sh.Pid)
|
|
}
|
|
}
|
|
|
|
if force {
|
|
con.PrintInfof("Shell %d killed (force)\n", sh.ID)
|
|
} else {
|
|
con.PrintInfof("Shell %d killed\n", sh.ID)
|
|
}
|
|
}
|
|
|
|
func runInteractive(cmd *cobra.Command, shellPath string, noPty bool, con *console.SliverClient, existing *managedShell) shellResult {
|
|
con.Println()
|
|
con.PrintInfof("Shell management: `shell ls`, `shell attach <id>`\n")
|
|
con.PrintInfof("Escape: press Ctrl-] to return to the Sliver client\n")
|
|
|
|
session := con.ActiveTarget.GetSession()
|
|
if session == nil {
|
|
return shellAttachFailed
|
|
}
|
|
|
|
if existing == nil {
|
|
con.PrintInfof("Opening shell tunnel ...\n\n")
|
|
} else {
|
|
con.PrintInfof("Re-attaching to shell %d ...\n\n", existing.ID)
|
|
}
|
|
|
|
var (
|
|
tunnel *core.TunnelIO
|
|
managed *managedShell
|
|
enablePTY bool
|
|
oldState *term.State
|
|
stateSaved bool
|
|
stopPtySize = func() {}
|
|
err error
|
|
)
|
|
|
|
if existing == nil {
|
|
// Create an RPC tunnel, then start it before binding the shell to the newly created tunnel.
|
|
ctxTunnel, cancelTunnel := context.WithCancel(context.Background())
|
|
defer cancelTunnel()
|
|
|
|
rpcTunnel, err := con.Rpc.CreateTunnel(ctxTunnel, &sliverpb.Tunnel{
|
|
SessionID: session.ID,
|
|
})
|
|
if err != nil {
|
|
con.PrintErrorf("%s\n", err)
|
|
return shellAttachFailed
|
|
}
|
|
log.Printf("Created new tunnel with id: %d, binding to shell ...", rpcTunnel.TunnelID)
|
|
|
|
// Start() takes an RPC tunnel and creates a local Reader/Writer tunnel object.
|
|
tunnel = core.GetTunnels().Start(rpcTunnel.TunnelID, rpcTunnel.SessionID)
|
|
|
|
var rows uint32
|
|
var cols uint32
|
|
if !noPty {
|
|
colsInt, rowsInt, err := term.GetSize(int(os.Stdout.Fd()))
|
|
if err != nil || rowsInt <= 0 || colsInt <= 0 {
|
|
colsInt, rowsInt, err = term.GetSize(int(os.Stdin.Fd()))
|
|
}
|
|
if err == nil && rowsInt > 0 && colsInt > 0 {
|
|
rows = uint32(rowsInt)
|
|
cols = uint32(colsInt)
|
|
}
|
|
}
|
|
|
|
shell, err := con.Rpc.Shell(context.Background(), &sliverpb.ShellReq{
|
|
Request: con.ActiveTarget.Request(cmd),
|
|
Path: shellPath,
|
|
EnablePTY: !noPty,
|
|
Rows: rows,
|
|
Cols: cols,
|
|
TunnelID: tunnel.ID,
|
|
})
|
|
if err != nil {
|
|
con.PrintErrorf("%s\n", err)
|
|
core.GetTunnels().Close(tunnel.ID)
|
|
return shellAttachFailed
|
|
}
|
|
if shell.Response != nil && shell.Response.Err != "" {
|
|
con.PrintErrorf("Error: %s\n", shell.Response.Err)
|
|
go backgroundCloseShell(con, tunnel.ID, session.ID)
|
|
return shellAttachFailed
|
|
}
|
|
|
|
path := shellPath
|
|
if path == "" {
|
|
path = shell.Path
|
|
}
|
|
enablePTY = !noPty
|
|
managed = &managedShell{
|
|
SessionID: session.ID,
|
|
SessionName: session.Name,
|
|
Path: path,
|
|
Pid: shell.Pid,
|
|
TunnelID: tunnel.ID,
|
|
EnablePTY: enablePTY,
|
|
CreatedAt: time.Now(),
|
|
state: shellStateAttached,
|
|
tunnel: tunnel,
|
|
output: newSwapWriter(os.Stdout),
|
|
}
|
|
managed.ID = shells.Add(managed)
|
|
managed.startReader(func() {
|
|
shells.Remove(managed.ID)
|
|
})
|
|
|
|
log.Printf("Bound remote shell pid %d to tunnel %d", shell.Pid, shell.TunnelID)
|
|
con.PrintInfof("Started remote shell [%d] with pid %d\n\n", managed.ID, shell.Pid)
|
|
} else {
|
|
managed = existing
|
|
tunnel = managed.Tunnel()
|
|
enablePTY = managed.EnablePTY
|
|
managed.setState(shellStateAttached)
|
|
managed.SetOutput(os.Stdout)
|
|
con.PrintInfof("Attached shell [%d] (pid %d)\n\n", managed.ID, managed.Pid)
|
|
}
|
|
|
|
if enablePTY {
|
|
oldState, err = term.MakeRaw(0)
|
|
log.Printf("Saving terminal state: %v", oldState)
|
|
if err != nil {
|
|
con.PrintErrorf("Failed to save terminal state\n")
|
|
managed.SetOutput(io.Discard)
|
|
managed.setState(shellStateDetached)
|
|
return shellAttachFailed
|
|
}
|
|
stateSaved = true
|
|
stopPtySize = startPtyResizeWatcher(con, cmd, tunnel.ID)
|
|
}
|
|
defer stopPtySize()
|
|
|
|
if stateSaved {
|
|
defer func() {
|
|
log.Printf("Restoring terminal state ...")
|
|
term.Restore(0, oldState)
|
|
}()
|
|
}
|
|
|
|
detached, _ := runAttachedIO(tunnel, con)
|
|
if detached {
|
|
managed.SetOutput(io.Discard)
|
|
managed.setState(shellStateDetached)
|
|
return shellDetached
|
|
}
|
|
|
|
managed.SetOutput(io.Discard)
|
|
managed.setState(shellStateClosing)
|
|
shells.Remove(managed.ID)
|
|
|
|
// "exit" should return immediately; tunnel close/cleanup is best-effort in the background.
|
|
go backgroundCloseShell(con, tunnel.ID, session.ID)
|
|
|
|
return shellExited
|
|
}
|
|
|
|
func backgroundCloseShell(con *console.SliverClient, tunnelID uint64, sessionID string) {
|
|
err := closeShellTunnel(con, tunnelID, sessionID)
|
|
if err != nil {
|
|
log.Printf("Background close tunnel %d failed: %v", tunnelID, err)
|
|
}
|
|
}
|
|
|
|
func closeShellTunnel(con *console.SliverClient, tunnelID uint64, sessionID string) error {
|
|
core.GetTunnels().Close(tunnelID)
|
|
if con == nil || con.Rpc == nil {
|
|
return nil
|
|
}
|
|
|
|
_, err := con.Rpc.CloseTunnel(context.Background(), &sliverpb.Tunnel{
|
|
TunnelID: tunnelID,
|
|
SessionID: sessionID,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func requestRemoteShellExit(tunnel *core.TunnelIO) {
|
|
if tunnel == nil || !tunnel.IsOpen() {
|
|
return
|
|
}
|
|
|
|
for _, payload := range [][]byte{[]byte("exit\n"), []byte("logout\n")} {
|
|
func(data []byte) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Printf("Shell exit write skipped: %v", r)
|
|
}
|
|
}()
|
|
select {
|
|
case tunnel.Send <- append([]byte(nil), data...):
|
|
default:
|
|
}
|
|
}(payload)
|
|
}
|
|
}
|
|
|
|
func sessionLabel(sessionID, sessionName string) string {
|
|
if sessionName == "" {
|
|
return sessionID
|
|
}
|
|
return sessionName + " (" + sessionID + ")"
|
|
}
|