From 0656885f83714594dbf1fb2736f58c359b85ac72 Mon Sep 17 00:00:00 2001 From: sudosammy Date: Sun, 26 Mar 2023 00:16:19 +0800 Subject: [PATCH 1/2] support multiple zone file entries for single fqdn --- .gitignore | 1 + README.md | 19 +++++++++++++---- libknary/dns.go | 7 ++++-- libknary/zones.go | 54 ++++++++++++++++++++++++++++++++++++----------- 4 files changed, 63 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 82f1ef5..df3a25c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ allowed.txt denied.txt blacklist.txt zone.txt +zones.txt knary.exe certs/*.json certs/*.crt diff --git a/README.md b/README.md index d653ad3..1122af9 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 ` 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 ` 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`. @@ -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. @@ -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: diff --git a/libknary/dns.go b/libknary/dns.go index ba99bd8..ae36165 100644 --- a/libknary/dns.go +++ b/libknary/dns.go @@ -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) { @@ -118,7 +118,10 @@ 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) + } + //m.Answer = append(m.Answer, zoneResponse) } // catch requests to pass through to burp diff --git a/libknary/zones.go b/libknary/zones.go index 2417a19..5a2a664 100644 --- a/libknary/zones.go +++ b/libknary/zones.go @@ -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) { @@ -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++ } @@ -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 } @@ -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) + } } } From c3fd6f34ac1fbddce9c9c58fe9b977de55c820f5 Mon Sep 17 00:00:00 2001 From: sudosammy Date: Sun, 26 Mar 2023 15:42:31 +0800 Subject: [PATCH 2/2] letsencrypt enviro variables --- examples/README.md | 9 +++++++-- libknary/certbot.go | 32 ++++++++++++++++++++++++++++---- libknary/dns.go | 1 - 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/examples/README.md b/examples/README.md index bcf548f..b601c51 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 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 + ## 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. diff --git a/libknary/certbot.go b/libknary/certbot.go index 83af393..e028c88 100644 --- a/libknary/certbot.go +++ b/libknary/certbot.go @@ -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" ) @@ -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, } } diff --git a/libknary/dns.go b/libknary/dns.go index ae36165..65c44f8 100644 --- a/libknary/dns.go +++ b/libknary/dns.go @@ -121,7 +121,6 @@ func parseDNS(m *dns.Msg, ipaddr string, EXT_IP string) { for _, element := range zoneResponse { m.Answer = append(m.Answer, element) } - //m.Answer = append(m.Answer, zoneResponse) } // catch requests to pass through to burp