Skip to content

Commit

Permalink
[+] #28 Support for dynamic port forwarding
Browse files Browse the repository at this point in the history
  • Loading branch information
WangYihang committed Jul 15, 2021
1 parent 46b8095 commit b3b7bcc
Show file tree
Hide file tree
Showing 11 changed files with 237 additions and 22 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,10 @@ First use `Jump` to select a client, then type `PTY`, then type `Interact` to dr
- [ ] Upgrade to Metepreter session
- [ ] Electron frontend
- [ ] [#53 Reload config file](https://github.com/WangYihang/Platypus/issues/53)
- [ ] [#28 Suport remote port forwarding](https://github.com/WangYihang/Platypus/issues/28))
- [ ] [#28 Suport dynamic port forwarding](https://github.com/WangYihang/Platypus/issues/28))
- [ ] [#28 Suport enable internet on the internal machine](https://github.com/WangYihang/Platypus/issues/28))
- [ ] Add version checking in Termite
- [x] [#28 Suport dynamic port forwarding](https://github.com/WangYihang/Platypus/issues/28))
- [x] [#28 Suport remote port forwarding](https://github.com/WangYihang/Platypus/issues/28))
- [x] [#28 Suport local port forwarding](https://github.com/WangYihang/Platypus/issues/28))
- [x] Design Private Protocol
- [x] Check exit state in WebSocket
Expand Down Expand Up @@ -254,7 +255,6 @@ First use `Jump` to select a client, then type `PTY`, then type `Interact` to dr
- [x] Upgrade common reverse shell session into full interactive session
- [x] Docker support (Added by [@yeya24](https://github.com/yeya24))


## Contributors

This project exists thanks to all the people who contribute.
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.14

require (
github.com/WangYihang/readline v0.0.0-20200229084751-518dcf4f57b3
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect
github.com/blang/semver v3.5.1+incompatible
github.com/creack/pty v1.1.11
Expand All @@ -24,6 +25,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4
github.com/rhysd/go-github-selfupdate v1.2.3
github.com/sevlyar/go-daemon v0.1.5
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ github.com/WangYihang/readline v0.0.0-20200229084751-518dcf4f57b3 h1:vnLf9dGMiEb
github.com/WangYihang/readline v0.0.0-20200229084751-518dcf4f57b3/go.mod h1:S7Ulsa01ssaZKuWV9IRCrAdCtCdI4dQIt39FYOzAEa4=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
Expand Down Expand Up @@ -244,6 +246,8 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand Down
19 changes: 13 additions & 6 deletions lib/cli/dispatcher/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,26 @@ func (dispatcher Dispatcher) Tunnel(args []string) {
case "pull":
local_address := fmt.Sprintf("%s:%d", dst_host, dst_port)
remote_address := fmt.Sprintf("%s:%d", src_host, src_port)
log.Info("Mapping remote (%s) to local (%s)", remote_address, local_address)
context.AddPullTunnelConfig(context.Ctx.CurrentTermite, local_address, remote_address)
case "push":
local_address := fmt.Sprintf("%s:%d", src_host, src_port)
remote_address := fmt.Sprintf("%s:%d", dst_host, dst_port)
log.Info("Mapping local (%s) to remote (%s)", local_address, remote_address)
context.AddPushTunnelConfig(context.Ctx.CurrentTermite, local_address, remote_address)
case "dynamic":
log.Error("TBD")
// context.AddDynamicTunnelConfig(context.Ctx.CurrentTermite, local_address, remote_address)
context.Ctx.CurrentTermite.StartSocks5Server()
case "internet":
log.Error("TBD")
// context.AddInternetTunnelConfig(context.Ctx.CurrentTermite, local_address, remote_address)
local_address := fmt.Sprintf("%s:%d", src_host, src_port)
remote_address := fmt.Sprintf("%s:%d", dst_host, dst_port)
if _, exists := context.Ctx.Socks5Servers[local_address]; exists {
log.Warn("Socks5 server (%s) already exists", local_address)
} else {
err := context.StartSocks5Server(local_address)
if err != nil {
log.Error("Starting local socks5 server failed: %s", err.Error())
} else {
context.AddPushTunnelConfig(context.Ctx.CurrentTermite, local_address, remote_address)
}
}
default:
log.Error("Invalid mode: %s, should be in {'Pull', 'Push', 'Dynamic', 'Internet'}", mode)
}
Expand Down
5 changes: 4 additions & 1 deletion lib/context/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ func CreateTCPClient(conn net.Conn, server *TCPServer) *TCPClient {

func (c *TCPClient) Close() {
c.conn.Close()
if Ctx.Current == c {
Ctx.Current = nil
}
}

func (c *TCPClient) GetConnString() string {
Expand Down Expand Up @@ -1001,7 +1004,7 @@ func (c *TCPClient) Upload(src string, dst string, broadcast bool) bool {
bar.IncrBy(segmentSize)
bar.DecoratorEwmaUpdate(time.Since(start))

if broadcast && i%64 == 0 {
if broadcast && i%0x10 == 0 {
c.NotifyWebSocketUploadingTermite(bytesSent, totalBytes)
}
}
Expand Down
23 changes: 23 additions & 0 deletions lib/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/WangYihang/Platypus/lib/util/str"
"github.com/WangYihang/Platypus/lib/util/ui"
"github.com/WangYihang/readline"
"github.com/armon/go-socks5"
"github.com/fatih/color"
"github.com/gin-gonic/gin"
"gopkg.in/olahol/melody.v1"
Expand Down Expand Up @@ -50,6 +51,7 @@ type Context struct {
PullTunnelInstance map[string]PullTunnelInstance
PushTunnelConfig map[string]PushTunnelConfig
PushTunnelInstance map[string]PushTunnelInstance
Socks5Servers map[string](*socks5.Server)
// Set later in platypus.go
Distributor *Distributor
RESTful *gin.Engine
Expand All @@ -72,6 +74,7 @@ func CreateContext() {
PullTunnelInstance: make(map[string]PullTunnelInstance),
PushTunnelConfig: make(map[string]PushTunnelConfig),
PushTunnelInstance: make(map[string]PushTunnelInstance),
Socks5Servers: make(map[string]*socks5.Server),
}
}
// Signal Handler
Expand Down Expand Up @@ -197,6 +200,7 @@ func Shutdown() {
}

func AddPushTunnelConfig(termite *TermiteClient, local_address string, remote_address string) {
log.Info("Mapping local (%s) to remote (%s)", local_address, remote_address)
termite.AtomLock.Lock()
defer func() { termite.AtomLock.Unlock() }()

Expand All @@ -220,6 +224,7 @@ func AddPushTunnelConfig(termite *TermiteClient, local_address string, remote_ad
}

func AddPullTunnelConfig(termite *TermiteClient, local_address string, remote_address string) {
log.Info("Mapping remote (%s) to local (%s)", remote_address, local_address)
tunnel, err := net.Listen("tcp", local_address)
if err != nil {
log.Error(err.Error())
Expand Down Expand Up @@ -277,6 +282,24 @@ func WriteTunnel(termite *TermiteClient, token string, data []byte) {
}
}

func StartSocks5Server(local_address string) error {
// Create tcp listener
socks5ServerListener, err := net.Listen("tcp", local_address)
if err != nil {
return err
}
// Create socks5 server
server, err := socks5.New(&socks5.Config{})
if err != nil {
return err
}
Ctx.Socks5Servers[local_address] = server
// Start socks5 server
go server.Serve(socks5ServerListener)
log.Success("Socks server started at: %s", local_address)
return nil
}

// func DeletePullTunnelConfig(local_host string, local_port uint16, remote_host string, remote_port uint16) {
// local_address := fmt.Sprintf("%s:%d", local_host, local_port)
// remote_address := fmt.Sprintf("%s:%d", remote_host, remote_port)
Expand Down
25 changes: 25 additions & 0 deletions lib/context/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/WangYihang/Platypus/lib/util/str"
humanize "github.com/dustin/go-humanize"
"github.com/jedib0t/go-pretty/table"
"github.com/phayes/freeport"
)

type WebSocketMessage struct {
Expand Down Expand Up @@ -471,10 +472,12 @@ func (s *TCPServer) AddTermiteClient(client *TermiteClient) {
log.Error("Duplicated income connection detected!")

// Respond to termite client that the client is duplicated
client.EncoderLock.Lock()
err := client.Encoder.Encode(message.Message{
Type: message.DUPLICATED_CLIENT,
Body: message.BodyDuplicateClient{},
})
client.EncoderLock.Unlock()
if err != nil {
// TODO: handle network error
log.Error("Network error: %s", err)
Expand Down Expand Up @@ -607,50 +610,58 @@ func TermiteMessageDispatcher(client *TermiteClient) {
conn, err := net.Dial("tcp", tc.Address)
if err != nil {
log.Error("Connecting to %s failed: %s", tc.Address, err.Error())
tc.Termite.EncoderLock.Lock()
tc.Termite.Encoder.Encode(message.Message{
Type: message.PUSH_TUNNEL_CONNECT_FAILED,
Body: message.BodyPushTunnelConnectFailed{
Token: token,
Reason: err.Error(),
},
})
tc.Termite.EncoderLock.Unlock()
} else {
log.Success("Connecting to %s succeed", tc.Address)
Ctx.PushTunnelInstance[token] = PushTunnelInstance{
Termite: tc.Termite,
Conn: &conn,
}
tc.Termite.EncoderLock.Lock()
tc.Termite.Encoder.Encode(message.Message{
Type: message.PUSH_TUNNEL_CONNECTED,
Body: message.BodyPushTunnelConnected{
Token: token,
},
})
tc.Termite.EncoderLock.Unlock()
go func() {
for {
buffer := make([]byte, 0x400)
n, err := conn.Read(buffer)
if err != nil {
log.Debug("Reading from %s failed: %s", tc.Address, err.Error())
tc.Termite.EncoderLock.Lock()
tc.Termite.Encoder.Encode(message.Message{
Type: message.PUSH_TUNNEL_DISCONNECTED,
Body: message.BodyPushTunnelDisonnected{
Token: token,
Reason: err.Error(),
},
})
tc.Termite.EncoderLock.Unlock()
conn.Close()
delete(Ctx.PushTunnelInstance, token)
break
} else {
log.Debug("%d bytes read from %s", n, tc.Address)
tc.Termite.EncoderLock.Lock()
tc.Termite.Encoder.Encode(message.Message{
Type: message.PUSH_TUNNEL_DATA,
Body: message.BodyPushTunnelData{
Token: token,
Data: buffer[0:n],
},
})
tc.Termite.EncoderLock.Unlock()
}
}
}()
Expand Down Expand Up @@ -703,19 +714,33 @@ func TermiteMessageDispatcher(client *TermiteClient) {
if ti, exists := Ctx.PushTunnelInstance[token]; exists {
_, err := (*ti.Conn).Write(data)
if err != nil {
ti.Termite.EncoderLock.Lock()
ti.Termite.Encoder.Encode(message.Message{
Type: message.PUSH_TUNNEL_CONNECT_FAILED,
Body: message.BodyPushTunnelConnectFailed{
Token: token,
Reason: err.Error(),
},
})
ti.Termite.EncoderLock.Unlock()
(*ti.Conn).Close()
delete(Ctx.PushTunnelInstance, token)
}
} else {
log.Debug("No such tunnel: %s", token)
}
case message.DYNAMIC_TUNNEL_CREATED:
port := msg.Body.(*message.BodyDynamicTunnelCreated).Port
local_address := fmt.Sprintf("127.0.0.1:%d", freeport.GetPort())
remote_address := fmt.Sprintf("127.0.0.1:%d", port)
log.Success("Mapping remote socks server (%s) into local address (%s)", remote_address, local_address)
AddPullTunnelConfig(
Ctx.CurrentTermite,
local_address,
remote_address,
)
case message.DYNAMIC_TUNNEL_CREATE_FAILED:
log.Error(msg.Body.(*message.BodyDynamicTunnelCreateFailed).Reason)
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions lib/context/termite.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ func (c *TermiteClient) GetHashFormat() string {
return c.server.hashFormat
}

func (c *TermiteClient) StartSocks5Server() {
c.EncoderLock.Lock()
c.Encoder.Encode(message.Message{
Type: message.DYNAMIC_TUNNEL_CREATE,
Body: message.BodyDynamicTunnelCreate{},
})
c.EncoderLock.Unlock()
}

func (c *TermiteClient) GatherClientInfo(hashFormat string) bool {
log.Info("Gathering information from termite client...")

Expand Down Expand Up @@ -299,6 +308,9 @@ func (c *TermiteClient) Close() {
}
}
c.conn.Close()
if Ctx.CurrentTermite == c {
Ctx.CurrentTermite = nil
}
}

func (c *TermiteClient) AsTable() {
Expand Down
26 changes: 26 additions & 0 deletions lib/util/message/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const (
PUSH_TUNNEL_CONNECT_FAILED
PUSH_TUNNEL_DISCONNECTED
PUSH_TUNNEL_DISCONNECT_FAILED
DYNAMIC_TUNNEL_CREATE
DYNAMIC_TUNNEL_DESTROY

// Termite -> Platypus
PROCESS_STARTED
Expand All @@ -40,6 +42,10 @@ const (
PUSH_TUNNEL_CREATE_FAILED
PUSH_TUNNEL_DELETED
PUSH_TUNNEL_DELETE_FAILED
DYNAMIC_TUNNEL_CREATED
DYNAMIC_TUNNEL_CREATE_FAILED
DYNAMIC_TUNNEL_DESTROIED
DYNAMIC_TUNNEL_DESTROY_FAILED
)

type Message struct {
Expand Down Expand Up @@ -175,6 +181,19 @@ type BodyPushTunnelDisonnectFailed struct {
Token string
}

type BodyDynamicTunnelCreate struct{}
type BodyDynamicTunnelCreated struct {
Port int
}
type BodyDynamicTunnelCreateFailed struct {
Reason string
}
type BodyDynamicTunnelDestroy struct{}
type BodyDynamicTunnelDestroied struct{}
type BodyDynamicTunnelDestroyFailed struct {
Reason string
}

func RegisterGob() {
// Client Management
gob.Register(&BodyClientInfo{})
Expand Down Expand Up @@ -208,4 +227,11 @@ func RegisterGob() {
gob.Register(&BodyPushTunnelDisonnect{})
gob.Register(&BodyPushTunnelDisonnected{})
gob.Register(&BodyPushTunnelDisonnectFailed{})
// Dynamic port forwarding
gob.Register(&BodyDynamicTunnelCreate{})
gob.Register(&BodyDynamicTunnelCreated{})
gob.Register(&BodyDynamicTunnelCreateFailed{})
gob.Register(&BodyDynamicTunnelDestroy{})
gob.Register(&BodyDynamicTunnelDestroied{})
gob.Register(&BodyDynamicTunnelDestroyFailed{})
}
2 changes: 1 addition & 1 deletion lib/util/ui/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

func PromptYesNo(message string) bool {
for {
fmt.Print(fmt.Sprintf("%s [Y/N] ", message))
fmt.Printf("%s [Y/N] ", message)
inputReader := bufio.NewReader(os.Stdin)
input, err := inputReader.ReadString('\n')
if err != nil {
Expand Down
Loading

0 comments on commit b3b7bcc

Please sign in to comment.