Skip to content

Commit

Permalink
Merge pull request #64 from sudosammy/fix-zone-file
Browse files Browse the repository at this point in the history
Fix zone file
  • Loading branch information
sudosammy authored Mar 26, 2023
2 parents 50c4fa0 + c3fd6f3 commit b845b8b
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 24 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ allowed.txt
denied.txt
blacklist.txt
zone.txt
zones.txt
knary.exe
certs/*.json
certs/*.crt
Expand Down
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

>Like "Canary" but more hipster, which means better 😎😎😎
knary is a canary token server that notifies a Slack/Discord/Teams/Lark/Telegram 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 allow/denylisting, working with Burp Collaborator, and automatic TLS certificate creation with Let's Encrypt.
knary is a canary token server that notifies a Slack/Discord/Teams/Lark/Telegram 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 allow/denylisting, working with Burp Collaborator, and automatic TLS certificate management with Let's Encrypt.

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

## Why is this useful?

Redteamers use canaries to be notified when someone (or *something*) attempts to interact with a server they control. Canaries help provide visibility over processes that were previously unknown. They can help find areas to probe for RFI or SSRF vulnerabilities, disclose previously unknown servers, provide evidence of an intercepting device, or just announce someone interacting with your server.
Offensive security teams use canaries to be notified when someone (or *something*) attempts to interact with a server they control. Canaries help provide visibility over processes that were previously unknown. They can help find areas to probe for RFI or SSRF vulnerabilities, disclose previously unknown servers, provide evidence of an intercepting device, or announce someone interacting with your server.

Defenders also use canaries as tripwires that can alert them of an attacker within their network by having the attacker announce themselves. If you are a defender, https://canarytokens.org might be what you’re looking for.

Expand All @@ -23,7 +23,9 @@ __Prerequisite:__ You need Go >=1.18 to build knary.
go install github.com/sudosammy/knary/v3@latest
```

**Important:** The specifics of how to perform the next two steps will depend on your domain registrar. Google `How to set Glue Record on <registrar name>` to get started. Ultimately, you need to configure your knary domain(s) to make use of itself as the nameserver (i.e. `ns1.knary.tld` and `ns2.knary.tld`) and configure Glue Records to point these nameservers back to your knary host. You may need to raise a support ticket to have this performed by your registrar.
See [here](#inbound-firewall-requirements) for guidance on which ports to open for knary.

**Important:** The specifics of how to perform the next two steps will depend on your domain registrar. Google `How to set Glue Record on <registrar name>` to get started. Ultimately, you need to configure your knary domain(s) to make use of itself as the nameserver (i.e. `ns1.knary.tld` and `ns2.knary.tld`) and configure a Glue Record to point these nameservers back to your knary host IP address. You may need to raise a support ticket to have this performed by your registrar.

2. Set your chosen knary domain(s) nameserver(s) to point to a subdomain under itself; such as `ns.knary.tld`. If required, set multiple nameserver records such as `ns1` and `ns2`.

Expand All @@ -33,7 +35,7 @@ go install github.com/sudosammy/knary/v3@latest

If your registry requires you to have multiple nameservers with **different** IP addresses, set the second nameserver to an IP address such as `8.8.8.8` or `1.1.1.1`.

4. This **will** take time to propagate, so go setup your [webhook(s)](#supported-webhook-configurations) while you wait. You can use [this tool](https://www.whatsmydns.net/#NS/) to check the propagation. Within a few hours you should see some DNS servers reflecting your knary domain as the nameserver.
4. This **will** take time to propagate (often several hours), so go setup your [webhook(s)](#supported-webhook-configurations) while you wait. You can use [this tool](https://www.whatsmydns.net/#NS/) to check the propagation. If you can't see at least some DNS servers reflecting your knary domain as the nameserver after 12 hours, you've done something wrong.

5. Create a `.env` file in the same directory as the knary binary and [configure](https://github.com/sudosammy/knary/tree/master/examples) it as necessary. You can also use environment variables to set these configurations. Environment variables will take precedence over the `.env` file.

Expand All @@ -43,6 +45,15 @@ If your registry requires you to have multiple nameservers with **different** IP

![knary go-ing](https://github.com/sudosammy/knary/raw/master/screenshots/run.png "knary go-ing")

## Inbound Firewall Requirements
In its most common configuration, knary will bind to these ports. You must permit connections from **any** IP address to these ports on your knary host.

| Port | Reason |
| --------| -------- |
| 53 tcp & udp | DNS |
| 80 tcp | HTTP |
| 443 tcp | HTTPS |

## Allowing or denying matches
You **will** find systems that spam your knary even long after an engagement has ended. You will also find several DNS requests to mundane subdomains hitting your knary every day. To stop these from cluttering your notifications knary has a few features:

Expand Down
9 changes: 7 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,17 @@ If you are running Burp Collaborator on the same server as knary, you will need
* `TLS_*` (CRT/KEY). If you're not using the `LETS_ENCRYPT` configuration use these environment variables to configure the location of your certificate and private key for accepting TLS (HTTPS) requests. Example input `TLS_KEY=certs/knary.key`
* `DEBUG` Enable/Disable displaying incoming requests in the terminal and some additional info. Default disabled (true/false)
* `ALLOWLIST_STRICT` Set to `true` to prevent fuzzy matching on allowlist items and only alert on exact matches
* `LE_ENV` Set to `staging` to use the Let's Encrypt's staging environment. Useful if you are testing configurations with Let's Encrypt and do not want to hit the rate limit
* `EXT_IP` The IP address the DNS canary will answer `A` questions with. By default knary will use the nameserver glue record. Setting this option will overrule that behaviour
* `DENYLIST_ALERTING` By default knary will alert on items in the denylist that haven't triggered in >14 days. Set to `false` to disable this behaviour
* `DNS_SUBDOMAIN` Tell knary to only notify on `*.<DNS_SUBDOMAIN>.<CANARY_DOMAIN>` DNS hits. This is useful if you your webhook is getting too noisy with DNS hits to your knary TLD and you do not maintain an allow or denylist. Setting this configuration will mimic how knary operated prior to version 3. Example input: `dns`
* `ZONE_FILE` knary supports responding to DNS requests based on an RFC 1034/1035 compliant zone file. Example input: `zone_file.txt`
* `DNS_RESOLVER` Tell cerbot to use a specific DNS server for checking the acme challenge.

## Optional Let's Encrypt Configurations
* `LE_ENV` Set to `staging` to use the Let's Encrypt's staging environment. Useful if you are testing configurations with Let's Encrypt and do not want to hit the rate limit
* `DNS_RESOLVER` Tell certbot to use a specific DNS server for checking the acme challenge
<!-- * `CERTBOT_TTL` The TTL of the TXT record created for validation of the certbot acme challenge. Default `120` seconds
* `CERTBOT_PROPAGATION_TIMEOUT` Seconds to attempt validation of a new certbot acme challenge. Default `60` seconds
* `CERTBOT_POLLING_INTERVAL` Frequency of attempts to validate a new certbot acme challenge up-to the value of `CERTBOT_PROPAGATION_TIMEOUT`. Default `2` seconds -->

## Note about editing configuration and Let's Encrypt
If you have previously been running knary with Let's Encrypt and have now configured Burp Collaborator or `DNS_SUBDOMAIN`, you should delete the files in the `certs/` folder so that knary can re-generate certificates that include these subdomains as a SAN. Otherwise knary may exhibit strange behaviour / failures when attempting to renew the certificate.
32 changes: 28 additions & 4 deletions libknary/certbot.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import (
"log"
"os"
"path/filepath"
"strconv"
"time"

"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/platform/config/env"
cmd "github.com/sudosammy/knary/v3/libknary/lego"
)

Expand All @@ -25,10 +25,34 @@ type Config struct {

// NewDefaultConfig returns a default configuration for the DNSProvider.
func NewDefaultConfig() *Config {
var confTTL int
var confTimeout time.Duration
var confPoll time.Duration

if value, ok := os.LookupEnv("CERTBOT_TTL"); ok {
confTTL, _ = strconv.Atoi(value)
} else {
confTTL = 120
}

if value, ok := os.LookupEnv("CERTBOT_PROPAGATION_TIMEOUT"); ok {
timeVal, _ := strconv.Atoi(value)
confTimeout = time.Duration(timeVal) * time.Second
} else {
confTimeout = 60 * time.Second
}

if value, ok := os.LookupEnv("CERTBOT_POLLING_INTERVAL"); ok {
timeVal, _ := strconv.Atoi(value)
confPoll = time.Duration(timeVal) * time.Second
} else {
confPoll = 2 * time.Second
}

return &Config{
TTL: env.GetOrDefaultInt("CERTBOT_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("CERTBOT_PROPAGATION_TIMEOUT", 1*time.Minute),
PollingInterval: env.GetOrDefaultSecond("CERTBOT_POLLING_INTERVAL", 2*time.Second),
TTL: confTTL,
PropagationTimeout: confTimeout,
PollingInterval: confPoll,
}
}

Expand Down
6 changes: 4 additions & 2 deletions libknary/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func goSendMsg(ipaddr, reverse, name, record string) bool {
}

if os.Getenv("DEBUG") == "true" {
Printy("Got A question for: "+name, 3)
Printy("Got "+record+" question for: "+name, 3)
}

if !inAllowlist(name, ipaddr) || inBlacklist(name, ipaddr) {
Expand Down Expand Up @@ -118,7 +118,9 @@ func parseDNS(m *dns.Msg, ipaddr string, EXT_IP string) {
// search zone file and append response if found
zoneResponse, foundInZone := inZone(q.Name, q.Qtype)
if foundInZone {
m.Answer = append(m.Answer, zoneResponse)
for _, element := range zoneResponse {
m.Answer = append(m.Answer, element)
}
}

// catch requests to pass through to burp
Expand Down
54 changes: 42 additions & 12 deletions libknary/zones.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import (
)

/*
LoadZone: Parse zone file and add to map
inZone: Take a question name and type and return dns.RR response + bool if found
LoadZone: Parse zone file and add to map
inZone: Take a question name and type and return dns.RR response + bool if found
*/
var zoneMap = map[string]dns.RR{}
var zoneMap = map[string]map[int]dns.RR{}
var fqdnCounter = map[string]int{}
var zoneCounter = 0

func LoadZone() (bool, error) {
Expand All @@ -34,7 +35,11 @@ func LoadZone() (bool, error) {
zp := dns.NewZoneParser(bufio.NewReader(zlist), "", "")

for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
zoneMap[rr.Header().Name] = rr
if zoneMap[rr.Header().Name] == nil {
zoneMap[rr.Header().Name] = map[int]dns.RR{}
}
zoneMap[rr.Header().Name][fqdnCounter[rr.Header().Name]] = rr
fqdnCounter[rr.Header().Name]++
zoneCounter++
}

Expand All @@ -51,12 +56,27 @@ func LoadZone() (bool, error) {
return true, nil
}

func inZone(needle string, qType uint16) (dns.RR, bool) {
if val, ok := zoneMap[strings.ToLower(needle)]; ok && val.Header().Rrtype == qType {
func inZone(needle string, qType uint16) (map[int]dns.RR, bool) {
if val, ok := zoneMap[strings.ToLower(needle)]; ok {
// this (sub)domain is present in the zone file
// confirm whether one or many match the qType
var appendKey int
returnMap := make(map[int]dns.RR)
for k := range zoneMap[strings.ToLower(needle)] {
if val[k].Header().Rrtype == qType {
returnMap[appendKey] = val[k]
appendKey++
}
}
// catch if there were no matching qTypes
if len(returnMap) == 0 {
return nil, false
}

if os.Getenv("DEBUG") == "true" {
Printy(needle+" found in zone file", 3)
Printy(needle+" found in zone file. Responding with "+strconv.Itoa(len(returnMap))+" response(s)", 3)
}
return val, true
return returnMap, true
}
return nil, false
}
Expand All @@ -69,17 +89,27 @@ func addZone(fqdn string, ttl int, qType string, value string) error {
return err
}

zoneMap[rr.Header().Name] = rr
nextVal := len(zoneMap[rr.Header().Name])
if zoneMap[rr.Header().Name] == nil {
zoneMap[rr.Header().Name] = map[int]dns.RR{}
}
zoneMap[rr.Header().Name][nextVal] = rr

if os.Getenv("DEBUG") == "true" {
Printy(fqdn+" "+qType+" added to zone", 3)
Printy(fqdn+" "+qType+" added to zone with ID: "+strconv.Itoa(nextVal), 3)
}
return nil
}

func remZone(fqdn string) {
_, ok := zoneMap[fqdn]
// this is pretty dodgy.
// we're hoping that the last zone added to the map is the one we want to delete
lastVal := len(zoneMap[fqdn]) - 1
_, ok := zoneMap[fqdn][lastVal]
if ok {
delete(zoneMap, fqdn)
delete(zoneMap[fqdn], lastVal)
if os.Getenv("DEBUG") == "true" {
Printy("Deleted "+fqdn+" with ID: "+strconv.Itoa(lastVal)+" from zone", 3)
}
}
}

0 comments on commit b845b8b

Please sign in to comment.