Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add telegram bot notification #3

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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

Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand All @@ -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):

```
Expand Down
237 changes: 140 additions & 97 deletions dnsobserver.go
Original file line number Diff line number Diff line change
@@ -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
}
}