Skip to content

Commit

Permalink
Merge pull request #11 from sudosammy/dev
Browse files Browse the repository at this point in the history
2.0.0 release
  • Loading branch information
sudosammy authored Jan 25, 2019
2 parents 21f154e + 545d9b0 commit 52b71a1
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 48 deletions.
46 changes: 21 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# knary - A simple HTTP(S) and DNS Canary Slackbot
# knary - A simple HTTP(S) and DNS Canary

>Like "Canary" but more hipster, which means better 😎😎😎
knary is a canary token server that notifies a Slack channel when incoming HTTP(S) or DNS requests match a given domain or any of its subdomains. It also supports functionality useful in offensive engagements including subdomain blacklisting.
knary is a canary token server that notifies a Slack channel (or other webhook) when incoming HTTP(S) or DNS requests match a given domain or any of its subdomains. It also supports functionality useful in offensive engagements including subdomain blacklisting.

![knary canary-ing](https://github.com/sudosammy/knary/raw/master/screenshots/canary.gif "knary canary-ing")

Expand All @@ -12,38 +12,19 @@ Redteamers use canaries to be notified when someone (or *something*) attempts to

Defenders also use canaries as tripwires that can alert them of an attacker within their network by having the attacker announce themselves. https://canarytokens.org offers a number of additional ways for defenders to use canaries.

### Why actually?

Because I wanted a project to help me learn Golang.

## Setup / Usage

1. Download the [applicable 64-bit knary binary](https://github.com/sudosammy/knary/releases) __OR__ build knary from source:

__Prerequisite:__ You need Go >=1.9 to build knary yourself. Ideally, use Go 1.10.x.
__Prerequisite:__ You need Go >=1.9 to build knary yourself. Ideally, use Go 1.11.x.
```
go get -u github.com/sudosammy/knary
```
2. Create an `A` record matching a subdomain wildcard (`*.mycanary.com`) to your server's IP address
3. Create an `NS` record matching `dns.mycanary.com` with `ns.mycanary.com` - knary will receive all DNS requests for `*.dns.mycanary.com`
4. You can self-sign the certificate for accepting TLS connections with something like `openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes`. However, some hosts might refuse to connect - so better you letsencrypt yourself a wildcard cert with something like `sudo certbot certonly --server https://acme-v02.api.letsencrypt.org/directory --manual --preferred-challenges dns -d *.mycanary.com`
5. Setup your [slack webhook](https://slack.com/apps/A0F7XDUAZ-incoming-webhooks)
6. Create a `.env` file in the same directory as the binary and [configure](https://github.com/sudosammy/knary#config-options) it as necessary:

```
DNS=true
HTTP=true
BIND_ADDR=0.0.0.0
CANARY_DOMAIN=mycanary.com
TLS_CRT=path/to/certificate.crt
TLS_KEY=path/to/private.key
DEBUG=false
LOG_FILE=knary.log
BLACKLIST_FILE=blacklist.txt
SLACK_WEBHOOK=https://hooks.slack.com/services/...
```
6. Create a `.env` file in the same directory as the binary and [configure](https://github.com/sudosammy/knary#config-options) it as necessary. Examples can be found in `examples/`
7. Run the binary (probably in `screen`, `tmux`, or similar because knary can't daemon _yet_) and hope for output that looks something like this:

![knary go-ing](https://github.com/sudosammy/knary/raw/master/screenshots/run.png "knary go-ing")
Expand All @@ -53,10 +34,11 @@ SLACK_WEBHOOK=https://hooks.slack.com/services/...
* DNS - `dig test.dns.mycanary.com`

## Blacklisting matches
You might find systems that spam your knary even long after an engagement has ended. To stop these from cluttering your Slack channel knary supports a blacklist (location specified in `.env`). Add the offending subdomains separated by a newline:
You might find systems that spam your knary even long after an engagement has ended. To stop these from cluttering your Slack channel knary supports a blacklist (location specified in `.env`). Add the offending subdomains or IP addresses separated by a newline:
```
www.mycanary.com
dns.mycanary.com
171.244.140.247
```
This would stop knary from alerting on `www.mycanary.com` but not `another.www.mycanary.com`. Changes to this file will come into effect immediately without requiring a knary restart.

Expand All @@ -67,9 +49,23 @@ This would stop knary from alerting on `www.mycanary.com` but not `another.www.m
* `CANARY_DOMAIN` The domain + TLD to match canary hits on. Example input: `mycanary.com` (knary will match `*.mycanary.com`)
* `TLS_*` The location of your certificate and private key necessary for accepting TLS (https) requests
* `DEBUG` Enable/Disable displaying incoming requests in the terminal and some additional info
* `SLACK_WEBHOOK` The full URL of the [incoming webhook](https://api.slack.com/custom-integrations/incoming-webhooks) for the Slack channel you want knary to notify
* `EXT_IP` __Optional__ The IP address the DNS canary will answer `A` questions with. By default knary will use the answer to `knary.{CANARY_DOMAIN}.`. Setting this option will overrule that behaviour
* `DNS_SERVER` __Optional__ The DNS server to use when asking `dns.{CANARY_DOMAIN}.`. This option is obsolete if `EXT_IP` is set. Default is Google's nameserver: `8.8.8.8`
* `LOG_FILE` __Optional__ Location for a file that knary will log timestamped matches and some errors. Example input: `/home/me/knary.log`
* `BLACKLIST_FILE` __Optional__ Location for a file containing subdomains (separated by newlines) that should be ignored by knary and not logged or posted to Slack. Example input: `blacklist.txt`
* `TIMEOUT` __Optional__ The timeout for reading the HTTP(S) request. Default is 2 seconds. Example input: `1`

### Webhooks
* `SLACK_WEBHOOK` The full URL of the [incoming webhook](https://api.slack.com/custom-integrations/incoming-webhooks) for the Slack channel you want knary to notify
* `DISCORD_WEBHOOK` __Optional__ The full URL of the [discord webhook](https://discordapp.com/developers/docs/resources/webhook) for the Discord channel you want knary to notify
* `PUSHOVER_TOKEN` __Optional__ The application token for the [Pushover Application](https://pushover.net/) you want knary to notify
* `PUSHOVER_USER` __Optional__ The user token of the Pushover user you want knary to nofify

### Burp Collaborator Config
If you are running Burp Collaborator on the same server as knary, you will need to configure the following.
* `BURP` __Optional__ Enable Burp Collaborator friendly mode
* `BURP_DOMAIN` The domain + TLD to match Collaborator hits on (e.g. `burp.{CANARY_DOMAIN}`). This needs to be an `NS` record much like the knary DNS configuration. See step 3. Example input: `burp.mycanary.com`
* `BURP_DNS_PORT` Local Burp Collaborator DNS port. This can't be 53, because knary listens on that one! Change Collaborator config to be something like 8053, and set this to `8053`
* `BURP_HTTP_PORT` Much like the above - set to `8080` (or whatever you set the Burp HTTP port to be)
* `BURP_HTTPS_PORT` Much like the above - set to `8443` (or whatever you set the Burp HTTPS port to be)
* `BURP_INT_IP` __Optional__ The internal IP address that Burp Collaborator is bound to. In most cases this will be `127.0.0.1` (which is the default); however, if you run knary in Docker you will need to set this to the Burp Collaborator IP address reachable from within the knary container
18 changes: 18 additions & 0 deletions examples/burp_env
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# RENAME ME TO .env
DNS=true
HTTP=true
BIND_ADDR=0.0.0.0
CANARY_DOMAIN=mydomain.ooo
TLS_CRT=certs/server.crt
TLS_KEY=certs/server.key
SLACK_WEBHOOK=https://hooks.slack.com/services/...

DEBUG=false
LOG_FILE=knary.log
BLACKLIST_FILE=blacklist.txt

BURP=true
BURP_DOMAIN=burp.mydomain.ooo
BURP_DNS_PORT=8053
BURP_HTTP_PORT=8080
BURP_HTTPS_PORT=8433
12 changes: 12 additions & 0 deletions examples/default_env
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# RENAME ME TO .env
DNS=true
HTTP=true
BIND_ADDR=0.0.0.0
CANARY_DOMAIN=mydomain.ooo
TLS_CRT=certs/server.crt
TLS_KEY=certs/server.key
SLACK_WEBHOOK=https://hooks.slack.com/services/...

DEBUG=false
LOG_FILE=knary.log
BLACKLIST_FILE=blacklist.txt
29 changes: 28 additions & 1 deletion libknary/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,38 @@ func parseDNS(m *dns.Msg, ipaddr string, EXT_IP string) {
for _, q := range m.Question {
// we only care about A questions
if q.Qtype == dns.TypeA {
//if we're in burp mode, we don't care about requests to the burp domain (and want to send them to the burp collab listener)
if os.Getenv("BURP") == "true" {
if strings.HasSuffix(strings.ToLower(q.Name), strings.ToLower(os.Getenv("BURP_DOMAIN"))+".") {
// to support our container friends - let the player choose the IP Burp is bound to
burpIP := ""
if os.Getenv("BURP_INT_IP") != "" {
burpIP = os.Getenv("BURP_INT_IP")
} else {
burpIP = "127.0.0.1"
}

c := dns.Client{}
newM := dns.Msg{}
newM.SetQuestion(q.Name, dns.TypeA)
r, _, err := c.Exchange(&newM, burpIP+":"+os.Getenv("BURP_DNS_PORT"))
if err != nil {
Printy(err.Error(), 2)
continue
}
m.Answer = r.Answer
//don't continue onto any other code paths if it's a collaborator message
if os.Getenv("DEBUG") == "true" {
Printy("Sent DNS to Burp: "+burpIP+":"+os.Getenv("BURP_DNS_PORT"), 3)
}
continue
}
}
if os.Getenv("DEBUG") == "true" {
Printy("DNS question for: "+q.Name, 3)
}

if !inBlacklist(q.Name) {
if !inBlacklist(q.Name, ipaddr) {
// spit the IP address to remove the port
// be wary of IPv6
ipSlice := strings.Split(ipaddr, ":")
Expand Down
123 changes: 109 additions & 14 deletions libknary/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package libknary

import (
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
"net/http/httputil"
"os"
"strconv"
"strings"
Expand All @@ -12,8 +15,68 @@ import (
)

func PrepareRequest() (net.Listener, net.Listener) {
// start listening on ports
ln80, err := net.Listen("tcp", os.Getenv("BIND_ADDR")+":80")
//assign bind address and ports
p80 := os.Getenv("BIND_ADDR") + ":80"
p443 := os.Getenv("BIND_ADDR") + ":443"

//if burp collab compatible env vars are detected:
//-re-assign ports
//-set up a reverse proxy to direct as needed
if os.Getenv("BURP") == "true" && os.Getenv("BURP_HTTP_PORT") != "" && os.Getenv("BURP_HTTPS_PORT") != "" {
p80 = "127.0.0.1:8880" // these are the local ports that knary
p443 = "127.0.0.1:8843" // will listen on as the client of the reverse proxy
// to support our container friends - let the player choose the IP Burp is bound to
burpIP := ""
if os.Getenv("BURP_INT_IP") != "" {
burpIP = os.Getenv("BURP_INT_IP")
} else {
burpIP = "127.0.0.1"
}

//start reverse proxy to direct requests appropriately
go func() {
e := http.ListenAndServe(os.Getenv("BIND_ADDR")+":80", &httputil.ReverseProxy{
Director: func(r *http.Request) {
r.URL.Scheme = "http"
//if the incoming request has the burp suffix send it to collab
if strings.HasSuffix(r.Host, os.Getenv("BURP_DOMAIN")) {
r.URL.Host = burpIP + ":" + os.Getenv("BURP_HTTP_PORT")
} else {
//otherwise send it raw to the local knary port
r.URL.Host = p80
r.Header.Set("X-Forwarded-For", r.RemoteAddr) //add port version of x-fwded for
}
},
})
if e != nil {
Printy(e.Error(), 2)
}
}()

go func() {
e := http.ListenAndServeTLS(os.Getenv("BIND_ADDR")+":443", os.Getenv("TLS_CRT"), os.Getenv("TLS_KEY"),
&httputil.ReverseProxy{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //it's localhost, we don't need to verify
},
Director: func(r *http.Request) {
r.URL.Scheme = "https"
//if the incoming request has the burp suffix send it to collab
if strings.HasSuffix(r.Host, os.Getenv("BURP_DOMAIN")) {
r.URL.Host = burpIP + ":" + os.Getenv("BURP_HTTPS_PORT")
} else {
//otherwise send it raw to the local knary port
r.URL.Host = p443
r.Header.Set("X-Forwarded-For", r.RemoteAddr)
}
},
})
if e != nil {
Printy(e.Error(), 2)
}
}()
}
ln80, err := net.Listen("tcp", p80)

if err != nil {
GiveHead(2)
Expand All @@ -29,7 +92,7 @@ func PrepareRequest() (net.Listener, net.Listener) {
}

config := &tls.Config{Certificates: []tls.Certificate{cer}}
ln443, err := tls.Listen("tcp", os.Getenv("BIND_ADDR")+":443", config)
ln443, err := tls.Listen("tcp", p443, config)

if err != nil {
GiveHead(2)
Expand All @@ -42,11 +105,9 @@ func PrepareRequest() (net.Listener, net.Listener) {
func AcceptRequest(ln net.Listener, wg *sync.WaitGroup) {
for {
conn, err := ln.Accept() // accept connections forever

if err != nil {
Printy(err.Error(), 2)
}

go handleRequest(conn)
}
wg.Done()
Expand Down Expand Up @@ -90,11 +151,22 @@ func handleRequest(conn net.Conn) {
host := ""
query := ""
userAgent := ""
fwd := ""

for _, header := range headers {
if stringContains(header, "Host") {
host = header
host = strings.TrimRight(header, "\r\n") + ":" + strconv.Itoa(lPort)
host = strings.TrimRight(header, "\r\n") + ":"
//using a reverse proxy, set ports back to the actual received ones
if os.Getenv("BURP") == "true" {
if lPort == 8880 {
host = host + "80"
} else if lPort == 8843 {
host = host + "443"
}
} else {
host = host + strconv.Itoa(lPort)
}
}
if stringContains(header, "OPTIONS") ||
stringContains(header, "GET") ||
Expand All @@ -107,17 +179,40 @@ func handleRequest(conn net.Conn) {
if stringContains(header, "User-Agent") {
userAgent = header
}
if stringContains(header, "X-Forwarded-For") {
//this is pretty funny, and also very irritating.
//Golang reverse proxy automagically adds the source IP address, but not the port.
//We add the value we want in the prepareRequest function,
//and strip off any values that don't have ports in this function.
//It's then reconstructed and appended to the message
val := strings.Split(header, ": ")[1]
srcAndPort := []string{}
mult := strings.Split(val, ",")
if len(mult) > 1 {
for _, srcaddr := range mult {
if strings.Contains(srcaddr, ":") {
srcAndPort = append(srcAndPort, srcaddr)
}
}
} else {
srcAndPort = mult
}
fwd = strings.Join(srcAndPort, "")
}
}

if !inBlacklist(host) {
go sendMsg(host +
"\n```" +
"Query: " + query + "\n" +
userAgent + "\n" +
"From: " + conn.RemoteAddr().String() +
"```")
if !inBlacklist(host, conn.RemoteAddr().String(), fwd) {
msg := fmt.Sprintf("%s\n```Query: %s\n%s\nFrom: %s", host, query, userAgent, conn.RemoteAddr().String())
if fwd != "" {
msg += "\nX-Forwarded-For: " + fwd
}
go sendMsg(msg + "```")

logger("[" + conn.RemoteAddr().String() + "]\n" + response)
if fwd != "" {
logger("[" + fwd + "]\n" + response)
} else {
logger("[" + conn.RemoteAddr().String() + "]\n" + response)
}
}
}
}
Expand Down
14 changes: 8 additions & 6 deletions libknary/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func CheckUpdate(version string, githubVersion string, githubURL string) bool {
return false
}

func inBlacklist(host string) bool {
func inBlacklist(needles ...string) bool {
if _, err := os.Stat(os.Getenv("BLACKLIST_FILE")); os.IsNotExist(err) {
if os.Getenv("DEBUG") == "true" {
Printy("Blacklist file does not exist - ignoring", 3)
Expand All @@ -79,12 +79,14 @@ func inBlacklist(host string) bool {
scanner := bufio.NewScanner(blklist)

for scanner.Scan() { // foreach blacklist item
if strings.Contains(host, scanner.Text()) && !strings.Contains(host, "."+scanner.Text()) {
// matches blacklist.domain but not x.blacklist.domain
if os.Getenv("DEBUG") == "true" {
Printy(scanner.Text()+" found in blacklist", 3)
for _, needle := range needles { // foreach needle
if strings.Contains(needle, scanner.Text()) && !strings.Contains(needle, "."+scanner.Text()) {
// matches blacklist.domain or 1.1.1.1 but not x.blacklist.domain
if os.Getenv("DEBUG") == "true" {
Printy(scanner.Text()+" found in blacklist", 3)
}
return true
}
return true
}
}
return false
Expand Down
18 changes: 18 additions & 0 deletions libknary/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,23 @@ func sendMsg(msg string) {
}
}

if os.Getenv("PUSHOVER_TOKEN") != "" && os.Getenv("PUSHOVER_USER") != "" {
jsonMsg := []byte(`{"token":"` + os.Getenv("PUSHOVER_TOKEN") + `","user":"` + os.Getenv("PUSHOVER_USER") + `","message":"` + msg + `"}`)
_, err := http.Post("https://api.pushover.net/1/messages.json/", "application/json", bytes.NewBuffer(jsonMsg))

if err != nil {
Printy(err.Error(), 2)
}
}

if os.Getenv("DISCORD_WEBHOOK") != "" {
jsonMsg := []byte(`{"username":"knary","content":"` + msg + `"}`)
_, err := http.Post(os.Getenv("DISCORD_WEBHOOK"), "application/json", bytes.NewBuffer(jsonMsg))

if err != nil {
Printy(err.Error(), 2)
}
}

// should be simple enough to add support for other webhooks here
}
Loading

0 comments on commit 52b71a1

Please sign in to comment.