mirror of
https://github.com/BishopFox/sliver
synced 2026-06-08 10:29:05 +00:00
149 lines
3.3 KiB
Go
149 lines
3.3 KiB
Go
package shell
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/bishopfox/sliver/client/console"
|
|
"github.com/bishopfox/sliver/client/core"
|
|
)
|
|
|
|
// runAttachedIO forwards local stdin to a shell tunnel while attached to the shell.
|
|
// It returns:
|
|
// - detached=true when user hit escape (Ctrl-])
|
|
// - closeRequested=true when user requested shell close (EOF/exit/logout)
|
|
func runAttachedIO(tunnel *core.TunnelIO, con *console.SliverClient) (detached bool, closeRequested bool) {
|
|
// We can't pipe stdin directly into the tunnel: if the gRPC tunnel send path blocks
|
|
// (network hang, server gone, etc.) tunnel.Write can block forever and "lock" the
|
|
// client in shell mode. Instead, buffer stdin locally and look for a local escape key.
|
|
const (
|
|
shellEscapeByte = byte(0x1d) // Ctrl-]
|
|
stdinBufSize = 32 * 1024
|
|
stdinQueueDepth = 128
|
|
)
|
|
|
|
stdinQueue := make(chan []byte, stdinQueueDepth)
|
|
stopWriter := make(chan struct{})
|
|
var writerWG sync.WaitGroup
|
|
writerWG.Add(1)
|
|
|
|
go func() {
|
|
defer writerWG.Done()
|
|
defer func() {
|
|
// Tunnel teardown can race with writes (server-side close, disconnect, etc.).
|
|
// Don't let a send-on-closed-channel panic bring down the whole client.
|
|
if r := recover(); r != nil {
|
|
log.Printf("Shell tunnel writer stopped: %v", r)
|
|
}
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-stopWriter:
|
|
return
|
|
case data, ok := <-stdinQueue:
|
|
if !ok {
|
|
return
|
|
}
|
|
if len(data) == 0 {
|
|
continue
|
|
}
|
|
select {
|
|
case <-stopWriter:
|
|
return
|
|
case tunnel.Send <- data:
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
defer func() {
|
|
close(stopWriter)
|
|
close(stdinQueue)
|
|
writerWG.Wait()
|
|
}()
|
|
|
|
stdin := newFilterReader(os.Stdin)
|
|
buf := make([]byte, stdinBufSize)
|
|
lineBuf := make([]byte, 0, 128)
|
|
inEscSeq := false
|
|
log.Printf("Reading from stdin (escape=Ctrl-]) ...")
|
|
|
|
for {
|
|
n, err := stdin.Read(buf)
|
|
if n > 0 {
|
|
data := buf[:n]
|
|
if i := bytes.IndexByte(data, shellEscapeByte); i >= 0 {
|
|
if i > 0 {
|
|
// Best-effort: don't block stdin if the tunnel is wedged.
|
|
dataCopy := append([]byte(nil), data[:i]...)
|
|
select {
|
|
case stdinQueue <- dataCopy:
|
|
default:
|
|
}
|
|
}
|
|
detached = true
|
|
break
|
|
}
|
|
|
|
exitRequested := false
|
|
for _, b := range data {
|
|
if inEscSeq {
|
|
if b >= 0x40 && b <= 0x7e {
|
|
inEscSeq = false
|
|
}
|
|
continue
|
|
}
|
|
|
|
switch b {
|
|
case '\r', '\n':
|
|
line := strings.TrimSpace(string(lineBuf))
|
|
if line == "exit" || line == "logout" {
|
|
exitRequested = true
|
|
}
|
|
lineBuf = lineBuf[:0]
|
|
case 0x1b:
|
|
inEscSeq = true
|
|
case 0x08, 0x7f:
|
|
if len(lineBuf) > 0 {
|
|
lineBuf = lineBuf[:len(lineBuf)-1]
|
|
}
|
|
default:
|
|
// Keep printable bytes only; ignore control sequences (arrows, etc.).
|
|
if b >= 0x20 && b <= 0x7e {
|
|
lineBuf = append(lineBuf, b)
|
|
}
|
|
}
|
|
}
|
|
|
|
dataCopy := append([]byte(nil), data...)
|
|
select {
|
|
case stdinQueue <- dataCopy:
|
|
default:
|
|
// Drop input if the tunnel send path is blocked; still allow escape.
|
|
}
|
|
|
|
if exitRequested {
|
|
closeRequested = true
|
|
break
|
|
}
|
|
}
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
con.PrintErrorf("Error reading from stdin: %s\n", err)
|
|
}
|
|
closeRequested = true
|
|
break
|
|
}
|
|
}
|
|
|
|
log.Printf("Exit interactive")
|
|
bufio.NewWriter(os.Stdout).Flush()
|
|
return detached, closeRequested
|
|
}
|