diff --git a/README.md b/README.md index acc22dd..dc4ca6f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # DNSObserver -A handy DNS service written in Go to aid in the detection of several types of blind vulnerabilities. It monitors a pentester's server for out-of-band DNS interactions and sends notifications with the received request's details via Slack. DNSObserver can help you find bugs such as blind OS command injection, blind SQLi, blind XXE, and many more! +A handy DNS service written in Go to aid in the detection of several types of blind vulnerabilities. It monitors a pentester's server for out-of-band DNS interactions and sends notifications with the received request's details via Slack and/or Telegram BOT. DNSObserver can help you find bugs such as blind OS command injection, blind SQLi, blind XXE, and many more! ![ScreenShot](https://raw.githubusercontent.com/allyomalley/dnsobserver/master/notification.png) @@ -16,6 +16,7 @@ What you'll need: * Your own registered domain name * A Virtual Private Server (VPS) to run the script on (I'm using Ubuntu - I have not tested this tool on other systems) * *[Optional]* Your own Slack workspace and a webhook +* *[Optional]* Your own Telegram BOT ### Domain and DNS Configuration @@ -66,6 +67,9 @@ Your VPS' public IP address. **webhook** *[Optional]* If you want to receive notifications, supply your Slack webhook URL. You'll be notified of any lookups of your domain name, or for any subdomains of your domain (I've excluded notifications for queries for any other apex domains and for your custom name servers to avoid excessive or random notifications). If you do not supply a webhook, interactions will be logged to standard output instead. Webhook setup instructions can be found [here](https://api.slack.com/messaging/webhooks). +**telegrambottoken** and **telegramchatid** *[Optional]* +If you want to receive notifications, supply your Telegram BOT Token and Chat ID. A quick guide can be found [here](https://gist.github.com/dideler/85de4d64f66c1966788c1b2304b9caf1). + **recordsFile** *[Optional]* By default, DNSObserver will only respond with an answer to queries for your domain name, or either of its name servers. For any other host, it will still notify you of the interaction (as long as it's your domain or a subdomain), but will send back an empty response. If you want DNSObserver to answer to A lookups for certain hosts with an address, you can either edit the config.yml file included in this project, or create your own based on this template: @@ -95,7 +99,7 @@ These settings mean that I want to respond to queries for 'google.com' with '1.2 Now, we are ready to start listening! If you want to be able to do other work on your VPS while DNSObserver runs, start up a new tmux session first. -For the standard setup, pass in the required arguments and your webhook: +For the standard setup, pass in the required arguments and your Slack webhook: ``` dnsobserver --domain example.com --ip 11.22.33.44 --webhook https://hooks.slack.com/services/XXX/XXX/XXX @@ -106,6 +110,11 @@ To achieve the above, but also include some custom A lookup responses, add the a dnsobserver --domain example.com --ip 11.22.33.44 --webhook https://hooks.slack.com/services/XXX/XXX/XXX --recordsFile my_records.yml ``` +Now if you want to get notification on your Telegram BOT. +``` +dnsobserver --domain example.com --ip 11.22.33.44 --telegrambottoken "0123456789:AAAAAAAAAAAAAAAA-BBBBBBBBBBBBBBBBBB" --telegramchatid 123456789 +``` + Assuming you've set everything up correctly, DNSObserver should now be running. To confirm it's working, open up a terminal on your desktop and perform a lookup of your new domain ('example.com' in this demo): ``` diff --git a/dnsobserver.go b/dnsobserver.go index 8241aaf..21b33b0 100644 --- a/dnsobserver.go +++ b/dnsobserver.go @@ -1,125 +1,168 @@ package main import ( - "flag" - "fmt" - "github.com/miekg/dns" - "github.com/slack-go/slack" - "gopkg.in/yaml.v2" - "io/ioutil" - "net" - "strings" - "time" + "flag" + "fmt" + "github.com/miekg/dns" + "github.com/slack-go/slack" + "gopkg.in/yaml.v2" + "io/ioutil" + "net" + "strings" + "time" + "bytes" + "encoding/json" + "errors" + "net/http" + "strconv" ) type Config struct { - Domain string `yaml:"domain"` - PublicIP string `yaml:"public_ip"` - SlackWebhook string `yaml:"webhook"` - Records []RR `yaml:"a_records"` + Domain string `yaml:"domain"` + PublicIP string `yaml:"public_ip"` + SlackWebhook string `yaml:"webhook"` + TelegramBotToken string `yaml:"telegrambottoken"` + TelegramChatID string `yaml:"telegramchatid"` + Records []RR `yaml:"a_records"` } type CustomRecords struct { - Records []RR `yaml:"a_records"` + Records []RR `yaml:"a_records"` } type RR struct { - Hostname string `yaml:"hostname"` - IP string `yaml:"ip"` + Hostname string `yaml:"hostname"` + IP string `yaml:"ip"` +} + +type sendMessageReqBody struct { + ChatID int64 `json:"chat_id"` + Text string `json:"text"` } var conf Config var answersMap map[string]string func sendSlack(message string) { - msg := slack.WebhookMessage{ - Text: message, - } - _ = slack.PostWebhook(conf.SlackWebhook, &msg) + msg := slack.WebhookMessage{ + Text: message, + } + _ = slack.PostWebhook(conf.SlackWebhook, &msg) +} + +func sendTelegram(chatID int64, message string) error { + reqBody := &sendMessageReqBody{ + ChatID: chatID, + Text: message, + } + + reqBytes, err := json.Marshal(reqBody) + if err != nil { + return err + } + + res, err := http.Post("https://api.telegram.org/bot"+conf.TelegramBotToken+"/sendMessage", "application/json", bytes.NewBuffer(reqBytes)) + if err != nil { + return err + } + if res.StatusCode != http.StatusOK { + return errors.New("unexpected status" + res.Status) + } + + return nil } func handleInteraction(w dns.ResponseWriter, r *dns.Msg) { - msg := dns.Msg{} - msg.SetReply(r) - remoteAddr := w.RemoteAddr().String() - q1 := r.Question[0] - t := time.Now() - - if dns.IsSubDomain(conf.Domain+".", q1.Name) && q1.Name != "ns1."+conf.Domain+"." && q1.Name != "ns2."+conf.Domain+"." { - addrParts := strings.Split(remoteAddr, ":") - dateString := "Received at: " + "`" + t.Format("January 2, 2006 3:04 PM") + "`" - fromString := "Received From: " + "`" + addrParts[0] + "`" - nameString := "Lookup Query: " + "`" + q1.Name + "`" - typeString := "Query Type: " + "`" + dns.TypeToString[q1.Qtype] + "`" - - message := "*Received DNS interaction:*" + "\n\n" + dateString + "\n" + fromString + "\n" + nameString + "\n" + typeString - if conf.SlackWebhook != "" { - sendSlack(message) - } else { - fmt.Println(message) - } - } - - switch r.Question[0].Qtype { - case dns.TypeA: - msg.Authoritative = true - domain := msg.Question[0].Name - address, ok := answersMap[domain] - if ok { - msg.Answer = append(msg.Answer, &dns.A{ - Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 1}, - A: net.ParseIP(address), - }) - } - } - w.WriteMsg(&msg) + msg := dns.Msg{} + msg.SetReply(r) + remoteAddr := w.RemoteAddr().String() + q1 := r.Question[0] + t := time.Now() + + if dns.IsSubDomain(conf.Domain+".", q1.Name) && q1.Name != "ns1."+conf.Domain+"." && q1.Name != "ns2."+conf.Domain+"." { + addrParts := strings.Split(remoteAddr, ":") + dateString := "Received at: " + "`" + t.Format("January 2, 2006 3:04 PM") + "`" + fromString := "Received From: " + "`" + addrParts[0] + "`" + nameString := "Lookup Query: " + "`" + q1.Name + "`" + typeString := "Query Type: " + "`" + dns.TypeToString[q1.Qtype] + "`" + + message := "*Received DNS interaction:*" + "\n\n" + dateString + "\n" + fromString + "\n" + nameString + "\n" + typeString + if conf.SlackWebhook != "" { + sendSlack(message) + } + if conf.TelegramBotToken != "" && conf.TelegramChatID != "" { + chatid, err := strconv.Atoi(conf.TelegramChatID) + if err != nil { + fmt.Println(err) + } + chatid64 := int64(chatid) + sendTelegram(chatid64, message) + } + fmt.Println(message) + } + + switch r.Question[0].Qtype { + case dns.TypeA: + msg.Authoritative = true + domain := msg.Question[0].Name + address, ok := answersMap[domain] + if ok { + msg.Answer = append(msg.Answer, &dns.A{ + Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 1}, + A: net.ParseIP(address), + }) + } + } + w.WriteMsg(&msg) } func loadConfig() { - domain := flag.String("domain", "", "Your registered domain name") - ip := flag.String("ip", "", "Your server's public IP address") - webhook := flag.String("webhook", "", "Your Slack webhook URL") - recordsPath := flag.String("recordsFile", "", "Optional path to custom records config file") - flag.Parse() - - conf = Config{Domain: *domain, PublicIP: *ip, SlackWebhook: *webhook} - if *recordsPath != "" { - recs := CustomRecords{} - data, err := ioutil.ReadFile(*recordsPath) - if err != nil { - panic(err) - } - err = yaml.Unmarshal(data, &recs) - if err != nil { - panic(err) - } - conf.Records = recs.Records - } - - answersMap = map[string]string{ - conf.Domain + ".": conf.PublicIP, - "ns1." + conf.Domain + ".": conf.PublicIP, - "ns2." + conf.Domain + ".": conf.PublicIP, - } - - for _, record := range conf.Records { - answersMap[record.Hostname+"."] = record.IP - } + domain := flag.String("domain", "", "Your registered domain name") + ip := flag.String("ip", "", "Your server's public IP address") + webhook := flag.String("webhook", "", "Your Slack webhook URL") + telegrambottoken := flag.String("telegrambottoken", "", "Your Telegram BOT Token") + telegramchatid := flag.String("telegramchatid", "", "Your Telegram Chat ID") + recordsPath := flag.String("recordsFile", "", "Optional path to custom records config file") + flag.Parse() + + conf = Config{Domain: *domain, PublicIP: *ip, SlackWebhook: *webhook, TelegramBotToken: *telegrambottoken, TelegramChatID: *telegramchatid} + if *recordsPath != "" { + recs := CustomRecords{} + data, err := ioutil.ReadFile(*recordsPath) + if err != nil { + panic(err) + } + err = yaml.Unmarshal(data, &recs) + if err != nil { + panic(err) + } + conf.Records = recs.Records + } + + answersMap = map[string]string{ + conf.Domain + ".": conf.PublicIP, + "ns1." + conf.Domain + ".": conf.PublicIP, + "ns2." + conf.Domain + ".": conf.PublicIP, + } + + for _, record := range conf.Records { + answersMap[record.Hostname+"."] = record.IP + } } func main() { - fmt.Println("Configuring...") - loadConfig() - if conf.Domain == "" || conf.PublicIP == "" { - fmt.Println("Error: Must supply a domain and public IP in config file") - return - } else { - fmt.Println("Listener starting!") - } - - dns.HandleFunc(".", handleInteraction) - if err := dns.ListenAndServe(conf.PublicIP+":53", "udp", nil); err != nil { - fmt.Println(err.Error()) - return - } + fmt.Println("Configuring...") + loadConfig() + if conf.Domain == "" || conf.PublicIP == "" { + fmt.Println("Error: Must supply a domain and public IP in config file") + return + } else { + fmt.Println("Listener starting!") + } + + dns.HandleFunc(".", handleInteraction) + if err := dns.ListenAndServe(conf.PublicIP+":53", "udp", nil); err != nil { + fmt.Println(err.Error()) + return + } }