Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ services:
- EXTERNAL_ADDRESS=<your.rssh.server.internal>:3232
- RSSH_CONSOLE_LABEL=c2.label
- RSSH_LOG_LEVEL=INFO # DISABLED, INFO, WARNING, ERROR, FATAL
- RSSH_WS_PATH=/ws
- RSSH_PUSH_PATH=/push
- SEED_AUTHORIZED_KEYS=${SSH_PUBLIC_KEY}
volumes:
- ./data:/data
Expand Down Expand Up @@ -221,6 +223,7 @@ This requires the web server component has been enabled.
--ntlm-proxy-creds Set NTLM proxy credentials in format DOMAIN\\USER:PASS
--owners Set owners of client, if unset client is public all users. E.g --owners jsmith,ldavidson
--proxy Set connect proxy address to bake it
--push-path Set HTTP(S) polling base path to bake into the client
--raw-download Download over raw TCP, outputs bash downloader rather than http
--shared-object Generate shared object file
--sni When TLS is in use, set a custom SNI for the client to connect with
Expand All @@ -230,6 +233,7 @@ This requires the web server component has been enabled.
--use-kerberos Instruct client to try and use kerberos ticket when using a proxy
--working-directory Set download/working directory for automatic script (i.e doing curl https://<url>.sh)
--ws Use plain http websockets as the underlying transport
--ws-path Set WebSocket transport path to bake into the client
--wss Use TLS websockets as the underlying transport
-C Comment to add as the public key (acts as the name)
-l List currently active download links
Expand Down Expand Up @@ -275,6 +279,21 @@ Or by baking it in with the `link` command.
ssh your.rssh.server -p 3232 link --ws --name test
```

WebSocket and HTTP(S) polling transports default to `/ws` and `/push`. If a
reverse proxy needs different paths, configure the same paths on the server and
client:

```sh
reverse_ssh --ws-path /socket --push-path /api/push :3232
./client -d ws://your.rssh.server:3232 --ws-path /socket
./client -d http://your.rssh.server:3232 --push-path /api/push
catcher$ link --ws --ws-path /socket --name test
catcher$ link --http --push-path /api/push --name test-http
```

The Docker entrypoint accepts these server options through `RSSH_WS_PATH` and
`RSSH_PUSH_PATH`.

### Bash autocomplete

The RSSH server has the `autocomplete` command which integrates nicely with bash so that you can have autocompletions when not using the server console.
Expand Down
16 changes: 16 additions & 0 deletions cmd/client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ var (
proxy string
ignoreInput string
customSNI string
wsPath string
pushPath string
// golang can only embed strings using the compile time linker
useHostKerberos string
logLevel string
Expand All @@ -64,6 +66,8 @@ func printHelp() {
fmt.Println("\t\t--ntlm-proxy-creds\tNTLM proxy credentials in format DOMAIN\\USER:PASS")
fmt.Println("\t\t--process_name\tProcess name shown in tasklist/process list")
fmt.Println("\t\t--sni\tWhen using TLS set the clients requested SNI to this value")
fmt.Println("\t\t--ws-path\tWebSocket transport path, defaults to /ws")
fmt.Println("\t\t--push-path\tHTTP(S) polling transport base path, defaults to /push")
fmt.Println("\t\t--log-level\tChange logging output levels, [INFO,WARNING,ERROR,FATAL,DISABLED]")
fmt.Println("\t\t--version-string\tSSH version string to use, i.e SSH-VERSION, defaults to internal.Version-runtime.GOOS_runtime.GOARCH")
fmt.Println("\t\t--private-key-path\tOptional path to unencrypted SSH key to use for connecting")
Expand All @@ -82,6 +86,8 @@ func makeInitialSettings() (*client.Settings, error) {
Addr: destination,
ProxyUseHostKerberos: useHostKerberos == "true",
SNI: customSNI,
WSPath: wsPath,
PushPath: pushPath,
VersionString: versionString,
}

Expand Down Expand Up @@ -175,6 +181,16 @@ func main() {
settings.SNI = userSpecifiedSNI
}

userSpecifiedWSPath, err := line.GetArgString("ws-path")
if err == nil {
settings.WSPath = userSpecifiedWSPath
}

userSpecifiedPushPath, err := line.GetArgString("push-path")
if err == nil {
settings.PushPath = userSpecifiedPushPath
}

timeoutInt := 180
timeout, err := line.GetArgString("connect-timeout")
if err == nil {
Expand Down
8 changes: 7 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ func printHelp() {
fmt.Println("\t--webserver\t\t(Depreciated) Enable webserver on the listen_address port")
fmt.Println("\t--enable-client-downloads\t\tEnable webserver and raw TCP to download clients")
fmt.Println("\t--external_address\tIf the external IP and port of the RSSH server is different from the listening address, set that here")
fmt.Println("\t--ws-path\t\tWebSocket transport path, defaults to /ws")
fmt.Println("\t--push-path\t\tHTTP(S) polling transport base path, defaults to /push")
fmt.Println("\t--timeout\t\tSet rssh client timeout (when a client is considered disconnected) defaults, in seconds, defaults to 5, if set to 0 timeout is disabled")
fmt.Println(" Utility")
fmt.Println("\t--fingerprint\t\tPrint fingerprint and exit. (Will generate server key if none exists)")
Expand All @@ -50,6 +52,8 @@ func main() {
"fingerprint": true,
"webserver": true, // deprecated
"enable-client-downloads": true,
"ws-path": true,
"push-path": true,
"datadir": true,
"h": true,
"help": true,
Expand Down Expand Up @@ -166,6 +170,8 @@ func main() {
tlskey, _ := options.GetArgString("tlskey")

enabledDownloads := options.IsSet("webserver") || options.IsSet("enable-client-downloads")
wsPath, _ := options.GetArgString("ws-path")
pushPath, _ := options.GetArgString("push-path")

if options.IsSet("webserver") {
log.Println("[WARNING] --webserver is deprecated, use --enable-client-downloads")
Expand Down Expand Up @@ -211,5 +217,5 @@ func main() {

log.Println("connect back: ", connectBackAddress)

server.Run(listenAddress, dataDir, connectBackAddress, autogeneratedConnectBack, tlscert, tlskey, insecure, enabledDownloads, tls, openproxy, timeout)
server.Run(listenAddress, dataDir, connectBackAddress, autogeneratedConnectBack, tlscert, tlskey, insecure, enabledDownloads, tls, openproxy, timeout, wsPath, pushPath)
}
20 changes: 19 additions & 1 deletion docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,22 @@ if [ ! -z "$SEED_AUTHORIZED_KEYS" ]; then
fi

cd /app/bin
exec ./server --datadir /data --enable-client-downloads --tls --external_address $EXTERNAL_ADDRESS :2222

server_args=(
--datadir /data
--enable-client-downloads
--tls
--external_address "$EXTERNAL_ADDRESS"
)

if [ -n "$RSSH_WS_PATH" ]; then
server_args+=(--ws-path "$RSSH_WS_PATH")
fi

if [ -n "$RSSH_PUSH_PATH" ]; then
server_args+=(--push-path "$RSSH_PUSH_PATH")
fi

server_args+=(:2222)

exec ./server "${server_args[@]}"
10 changes: 8 additions & 2 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/NHAS/reverse_ssh/internal/client/handlers"
"github.com/NHAS/reverse_ssh/internal/client/keys"
"github.com/NHAS/reverse_ssh/pkg/logger"
"github.com/NHAS/reverse_ssh/pkg/transport"
"github.com/bodgit/ntlmssp"
"golang.org/x/crypto/ssh"
socks "golang.org/x/net/proxy"
Expand Down Expand Up @@ -305,6 +306,8 @@ type Settings struct {
Fingerprint string
ProxyAddr string
SNI string
WSPath string
PushPath string

ProxyUseHostKerberos bool

Expand Down Expand Up @@ -344,6 +347,9 @@ func Run(settings *Settings) {
l := logger.NewLog("client")

var err error
settings.WSPath = transport.NormalizePath(settings.WSPath, transport.DefaultWSPath)
settings.PushPath = transport.NormalizePath(settings.PushPath, transport.DefaultPushPath)

settings.ProxyAddr, err = GetProxyDetails(settings.ProxyAddr)
if err != nil {
log.Fatal("Invalid proxy details", settings.ProxyAddr, ":", err)
Expand Down Expand Up @@ -462,7 +468,7 @@ func Run(settings *Settings) {

switch scheme {
case "wss", "ws":
c, err := websocket.NewConfig("ws://"+realAddr+"/ws", "ws://"+realAddr)
c, err := websocket.NewConfig("ws://"+realAddr+settings.WSPath, "ws://"+realAddr)
if err != nil {
log.Println("Could not create websockets configuration: ", err)
<-time.After(10 * time.Second)
Expand All @@ -483,7 +489,7 @@ func Run(settings *Settings) {
conn = wsConn
case "http", "https":

conn, err = NewHTTPConn(scheme+"://"+realAddr, func() (net.Conn, error) {
conn, err = NewHTTPConnWithPushPath(scheme+"://"+realAddr, settings.PushPath, func() (net.Conn, error) {
return Connect(realAddr, settings.ProxyAddr, settings.ConnectTimeout, settings.ProxyUseHostKerberos, settings.ntlm)
})

Expand Down
29 changes: 24 additions & 5 deletions internal/client/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/NHAS/reverse_ssh/internal/client/keys"
"github.com/NHAS/reverse_ssh/pkg/mux"
"github.com/NHAS/reverse_ssh/pkg/transport"
)

type HTTPConn struct {
Expand All @@ -29,16 +30,22 @@ type HTTPConn struct {
// Cache buster for middleware proxies
start int

client *http.Client
pushPath string
client *http.Client
}

func NewHTTPConn(address string, connector func() (net.Conn, error)) (*HTTPConn, error) {
return NewHTTPConnWithPushPath(address, transport.DefaultPushPath, connector)
}

func NewHTTPConnWithPushPath(address, pushPath string, connector func() (net.Conn, error)) (*HTTPConn, error) {

result := &HTTPConn{
done: make(chan interface{}),
readBuffer: mux.NewSyncBuffer(8096),
address: address,
start: mathrand.Int(),
pushPath: transport.NormalizePath(pushPath, transport.DefaultPushPath),
}

result.client = &http.Client{
Expand All @@ -62,9 +69,9 @@ func NewHTTPConn(address string, connector func() (net.Conn, error)) (*HTTPConn,

publicKeyBytes := s.PublicKey().Marshal()

resp, err := result.client.Head(address + "/push?key=" + hex.EncodeToString(publicKeyBytes))
resp, err := result.client.Head(result.initURL(hex.EncodeToString(publicKeyBytes)))
if err != nil {
return nil, fmt.Errorf("failed to connect to %s/push?key=%s, err: %s", address, hex.EncodeToString(publicKeyBytes), err)
return nil, fmt.Errorf("failed to connect to %s, err: %s", result.initURL(hex.EncodeToString(publicKeyBytes)), err)
}
resp.Body.Close()

Expand All @@ -90,6 +97,18 @@ func NewHTTPConn(address string, connector func() (net.Conn, error)) (*HTTPConn,
return result, nil
}

func (c *HTTPConn) initURL(publicKeyHex string) string {
return c.address + c.pushPath + "?key=" + publicKeyHex
}

func (c *HTTPConn) readURL() string {
return c.address + transport.JoinPushPath(c.pushPath, strconv.Itoa(c.start)) + "?id=" + c.ID
}

func (c *HTTPConn) writeURL() string {
return c.address + c.pushPath + "?id=" + c.ID
}

func (c *HTTPConn) startReadLoop() {
for {
select {
Expand All @@ -99,7 +118,7 @@ func (c *HTTPConn) startReadLoop() {
default:
}

resp, err := c.client.Get(c.address + "/push/" + strconv.Itoa(c.start) + "?id=" + c.ID)
resp, err := c.client.Get(c.readURL())
if err != nil {
log.Println("error getting data: ", err)
c.Close()
Expand Down Expand Up @@ -142,7 +161,7 @@ func (c *HTTPConn) Write(b []byte) (n int, err error) {
default:
}

resp, err := c.client.Post(c.address+"/push?id="+c.ID, "application/octet-stream", bytes.NewBuffer(b))
resp, err := c.client.Post(c.writeURL(), "application/octet-stream", bytes.NewBuffer(b))
if err != nil {
c.Close()
return 0, err
Expand Down
41 changes: 41 additions & 0 deletions internal/client/http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package client

import "testing"

func TestHTTPConnURLsUseDefaultPushPath(t *testing.T) {
c := &HTTPConn{
ID: "session",
address: "https://example.internal",
pushPath: "/push",
start: 7,
}

if got := c.initURL("abc123"); got != "https://example.internal/push?key=abc123" {
t.Fatalf("initURL = %q", got)
}
if got := c.readURL(); got != "https://example.internal/push/7?id=session" {
t.Fatalf("readURL = %q", got)
}
if got := c.writeURL(); got != "https://example.internal/push?id=session" {
t.Fatalf("writeURL = %q", got)
}
}

func TestHTTPConnURLsUseCustomPushPath(t *testing.T) {
c := &HTTPConn{
ID: "session",
address: "https://example.internal",
pushPath: "/api/poll",
start: 9,
}

if got := c.initURL("abc123"); got != "https://example.internal/api/poll?key=abc123" {
t.Fatalf("initURL = %q", got)
}
if got := c.readURL(); got != "https://example.internal/api/poll/9?id=session" {
t.Fatalf("readURL = %q", got)
}
if got := c.writeURL(); got != "https://example.internal/api/poll?id=session" {
t.Fatalf("writeURL = %q", got)
}
}
12 changes: 12 additions & 0 deletions internal/server/commands/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ func (l *link) ValidArgs() map[string]string {
"stdio": "Use stdin and stdout as transport, will disable logging, destination after stdio:// is ignored",
"http": "Use http polling as the underlying transport",
"https": "Use https polling as the underlying transport",
"ws-path": "Set WebSocket transport path to bake into the client",
"push-path": "Set HTTP(S) polling base path to bake into the client",
"use-host-header": "Use HTTP Host header as callback address when generating download template (add .sh to your download urls and find out)",
"shared-object": "Generate shared object file",
"fingerprint": "Set RSSH server fingerprint will default to server public key",
Expand Down Expand Up @@ -212,6 +214,16 @@ func (l *link) Run(user *users.User, tty io.ReadWriter, line terminal.ParsedLine
return err
}

buildConfig.WSPath, err = line.GetArgString("ws-path")
if err != nil && err != terminal.ErrFlagNotSet {
return err
}

buildConfig.PushPath, err = line.GetArgString("push-path")
if err != nil && err != terminal.ErrFlagNotSet {
return err
}

buildConfig.LogLevel, err = line.GetArgString("log-level")
if err != nil {
if err != terminal.ErrFlagNotSet {
Expand Down
4 changes: 3 additions & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func CreateOrLoadServerKeys(privateKeyPath string) (ssh.Signer, error) {
return private, nil
}

func Run(addr, dataDir, connectBackAddress string, autogeneratedConnectBack bool, TLSCertPath, TLSKeyPath string, insecure, enabledDownloads, enabletTLS, openproxy bool, timeout int) {
func Run(addr, dataDir, connectBackAddress string, autogeneratedConnectBack bool, TLSCertPath, TLSKeyPath string, insecure, enabledDownloads, enabletTLS, openproxy bool, timeout int, wsPath, pushPath string) {
c := mux.MultiplexerConfig{
Control: true,
Downloads: enabledDownloads,
Expand All @@ -56,6 +56,8 @@ func Run(addr, dataDir, connectBackAddress string, autogeneratedConnectBack bool
TLSKeyPath: TLSKeyPath,
AutoTLSCommonName: connectBackAddress,
TcpKeepAlive: timeout,
WSPath: wsPath,
PushPath: pushPath,
PollingAuthChecker: func(key string, addr net.Addr) bool {

authorizedKey, err := hex.DecodeString(key)
Expand Down
4 changes: 2 additions & 2 deletions internal/server/webserver/buildmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type BuildConfig struct {

ConnectBackAdress, Fingerprint string

Proxy, SNI, LogLevel string
Proxy, SNI, WSPath, PushPath, LogLevel string

UseKerberosAuth bool

Expand Down Expand Up @@ -183,7 +183,7 @@ func Build(config BuildConfig) (string, error) {
return "", err
}

buildArguments = append(buildArguments, fmt.Sprintf("-ldflags=-s -w -X main.logLevel=%s -X main.destination=%s -X main.fingerprint=%s -X main.proxy=%s -X main.customSNI=%s -X main.useHostKerberos=%t -X main.ntlmProxyCreds=%s -X main.versionString=%s -X github.com/NHAS/reverse_ssh/internal.Version=%s", config.LogLevel, config.ConnectBackAdress, config.Fingerprint, config.Proxy, config.SNI, config.UseKerberosAuth, config.NTLMProxyCreds, strings.TrimSpace(config.VersionString), strings.TrimSpace(f.Version)))
buildArguments = append(buildArguments, fmt.Sprintf("-ldflags=-s -w -X main.logLevel=%s -X main.destination=%s -X main.fingerprint=%s -X main.proxy=%s -X main.customSNI=%s -X main.wsPath=%s -X main.pushPath=%s -X main.useHostKerberos=%t -X main.ntlmProxyCreds=%s -X main.versionString=%s -X github.com/NHAS/reverse_ssh/internal.Version=%s", config.LogLevel, config.ConnectBackAdress, config.Fingerprint, config.Proxy, config.SNI, config.WSPath, config.PushPath, config.UseKerberosAuth, config.NTLMProxyCreds, strings.TrimSpace(config.VersionString), strings.TrimSpace(f.Version)))
buildArguments = append(buildArguments, "-o", f.FilePath, filepath.Join(projectRoot, "/cmd/client"))

cmd := exec.Command(buildTool, buildArguments...)
Expand Down
Loading