init dns.proxy

This commit is contained in:
buffermet 2024-10-04 03:00:54 +02:00
parent e190737c91
commit a49d561dce
6 changed files with 736 additions and 0 deletions

View file

@ -0,0 +1,124 @@
package dns_proxy
import (
"github.com/bettercap/bettercap/v2/session"
)
type DnsProxy struct {
session.SessionModule
proxy *DNSProxy
}
func (mod *DnsProxy) Author() string {
return "Yarwin Kolff <@buffermet>"
}
func (mod *DnsProxy) Configure() error {
var err error
var address string
var dnsPort int
var doRedirect bool
var nameserver string
var netProtocol string
var proxyPort int
var scriptPath string
if mod.Running() {
return session.ErrAlreadyStarted(mod.Name())
} else if err, dnsPort = mod.IntParam("dns.port"); err != nil {
return err
} else if err, address = mod.StringParam("dns.proxy.address"); err != nil {
return err
} else if err, nameserver = mod.StringParam("dns.proxy.nameserver"); err != nil {
return err
} else if err, netProtocol = mod.StringParam("dns.proxy.networkprotocol"); err != nil {
return err
} else if err, proxyPort = mod.IntParam("dns.proxy.port"); err != nil {
return err
} else if err, doRedirect = mod.BoolParam("dns.proxy.redirect"); err != nil {
return err
} else if err, scriptPath = mod.StringParam("dns.proxy.script"); err != nil {
return err
}
error := mod.proxy.Configure(address, dnsPort, doRedirect, nameserver, netProtocol, proxyPort, scriptPath)
return error
}
func (mod *DnsProxy) Description() string {
return "A full featured DNS proxy that can be used to manipulate DNS traffic."
}
func (mod *DnsProxy) Name() string {
return "dns.proxy"
}
func NewDnsProxy(s *session.Session) *DnsProxy {
mod := &DnsProxy{
SessionModule: session.NewSessionModule("dns.proxy", s),
proxy: NewDNSProxy(s, "dns.proxy"),
}
mod.AddParam(session.NewIntParameter("dns.port",
"53",
"DNS port to redirect when the proxy is activated."))
mod.AddParam(session.NewStringParameter("dns.proxy.address",
session.ParamIfaceAddress,
session.IPv4Validator,
"Address to bind the DNS proxy to."))
mod.AddParam(session.NewStringParameter("dns.proxy.nameserver",
"1.1.1.1",
session.IPv4Validator,
"DNS resolver address."))
mod.AddParam(session.NewStringParameter("dns.proxy.networkprotocol",
"udp",
"^(udp|tcp|tcp-tls)$",
"Network protocol for the DNS proxy server to use. Accepted values: udp, tcp, tcp-tls"))
mod.AddParam(session.NewIntParameter("dns.proxy.port",
"8053",
"Port to bind the DNS proxy to."))
mod.AddParam(session.NewBoolParameter("dns.proxy.redirect",
"true",
"Enable or disable port redirection with iptables."))
mod.AddParam(session.NewStringParameter("dns.proxy.script",
"",
"",
"Path of a JS proxy script."))
mod.AddHandler(session.NewModuleHandler("dns.proxy on", "",
"Start the DNS proxy.",
func(args []string) error {
return mod.Start()
}))
mod.AddHandler(session.NewModuleHandler("dns.proxy off", "",
"Stop the DNS proxy.",
func(args []string) error {
return mod.Stop()
}))
return mod
}
func (mod *DnsProxy) Start() error {
if err := mod.Configure(); err != nil {
return err
}
return mod.SetRunning(true, func() {
mod.proxy.Start()
})
}
func (mod *DnsProxy) Stop() error {
return mod.SetRunning(false, func() {
mod.proxy.Stop()
})
}

View file

@ -0,0 +1,194 @@
package dns_proxy
import (
"context"
"fmt"
"strings"
"time"
"github.com/bettercap/bettercap/v2/firewall"
"github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/log"
"github.com/miekg/dns"
)
const (
dialTimeout = 2 * time.Second
readTimeout = 2 * time.Second
writeTimeout = 2 * time.Second
)
type DNSProxy struct {
Address string
Name string
Nameserver string
NetProtocol string
Redirection *firewall.Redirection
Script *DnsProxyScript
Server *dns.Server
Sess *session.Session
doRedirect bool
isRunning bool
tag string
}
func (p *DNSProxy) Configure(address string, dnsPort int, doRedirect bool, nameserver string, netProtocol string, proxyPort int, scriptPath string) error {
var err error
p.Address = address
p.doRedirect = doRedirect
if scriptPath != "" {
if err, p.Script = LoadDnsProxyScript(scriptPath, p.Sess); err != nil {
return err
} else {
p.Debug("proxy script %s loaded.", scriptPath)
}
} else {
p.Script = nil
}
dnsClient := dns.Client{
DialTimeout: dialTimeout,
Net: netProtocol,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
}
resolverAddr := fmt.Sprintf("%s:%d", nameserver, dnsPort)
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
m := new(dns.Msg)
m.SetReply(req)
clientIP := strings.Split(w.RemoteAddr().String(), ":")[0]
req, res := p.onRequestFilter(req, clientIP)
if res == nil {
// unused var is time til res
res, _, err := dnsClient.Exchange(req, resolverAddr)
if err != nil {
p.Debug("error while resolving DNS query: %s", err.Error())
m.SetRcode(req, dns.RcodeServerFailure)
w.WriteMsg(m)
return
}
res = p.onResponseFilter(req, res, clientIP)
if res == nil {
p.Error("response filter returned nil")
m.SetRcode(req, dns.RcodeServerFailure)
w.WriteMsg(m)
} else {
if err := w.WriteMsg(res); err != nil {
p.Error("Error writing response: %s", err)
}
}
} else {
if err := w.WriteMsg(res); err != nil {
p.Error("Error writing response: %s", err)
}
}
})
p.Server = &dns.Server{
Addr: fmt.Sprintf("%s:%d", address, proxyPort),
Net: netProtocol,
Handler: handler,
}
if p.doRedirect {
if !p.Sess.Firewall.IsForwardingEnabled() {
p.Info("enabling forwarding.")
p.Sess.Firewall.EnableForwarding(true)
}
p.Redirection = firewall.NewRedirection(p.Sess.Interface.Name(),
netProtocol,
dnsPort,
p.Address,
proxyPort)
if err := p.Sess.Firewall.EnableRedirection(p.Redirection, true); err != nil {
return err
}
p.Debug("applied redirection %s", p.Redirection.String())
} else {
p.Warning("port redirection disabled, the proxy must be set manually to work")
}
p.Sess.UnkCmdCallback = func(cmd string) bool {
if p.Script != nil {
return p.Script.OnCommand(cmd)
}
return false
}
return nil
}
func (p *DNSProxy) dnsWorker() error {
p.isRunning = true
return p.Server.ListenAndServe()
}
func (p *DNSProxy) Debug(format string, args ...interface{}) {
p.Sess.Events.Log(log.DEBUG, p.tag+format, args...)
}
func (p *DNSProxy) Info(format string, args ...interface{}) {
p.Sess.Events.Log(log.INFO, p.tag+format, args...)
}
func (p *DNSProxy) Warning(format string, args ...interface{}) {
p.Sess.Events.Log(log.WARNING, p.tag+format, args...)
}
func (p *DNSProxy) Error(format string, args ...interface{}) {
p.Sess.Events.Log(log.ERROR, p.tag+format, args...)
}
func (p *DNSProxy) Fatal(format string, args ...interface{}) {
p.Sess.Events.Log(log.FATAL, p.tag+format, args...)
}
func NewDNSProxy(s *session.Session, tag string) *DNSProxy {
p := &DNSProxy{
Name: "dns.proxy",
Sess: s,
Server: nil,
doRedirect: true,
tag: session.AsTag(tag),
}
return p
}
func (p *DNSProxy) Start() {
go func() {
p.Info("started on %s", p.Server.Addr)
err := p.dnsWorker()
// TODO: check the dns server closed error
if err != nil && err.Error() != "dns: Server closed" {
p.Fatal("%s", err)
}
}()
}
func (p *DNSProxy) Stop() error {
if p.doRedirect && p.Redirection != nil {
p.Debug("disabling redirection %s", p.Redirection.String())
if err := p.Sess.Firewall.EnableRedirection(p.Redirection, false); err != nil {
return err
}
p.Redirection = nil
}
p.Sess.UnkCmdCallback = nil
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return p.Server.ShutdownContext(ctx)
}

View file

@ -0,0 +1,86 @@
package dns_proxy
import (
"strings"
"github.com/miekg/dns"
)
func shortenResourceRecords(records []string) []string {
shorterRecords := []string{}
for _, record := range records {
shorterRecord := strings.ReplaceAll(record, "\t", " ")
shorterRecords = append(shorterRecords, shorterRecord)
}
return shorterRecords
}
func (p *DNSProxy) logRequestAction(j *JSQuery) {
p.Sess.Events.Add(p.Name+".spoofed-request", struct {
Client string
Questions []string
}{
j.Client["IP"],
shortenResourceRecords(j.Questions),
})
}
func (p *DNSProxy) logResponseAction(j *JSQuery) {
p.Sess.Events.Add(p.Name+".spoofed-response", struct {
client string
Extras []string
Answers []string
Nameservers []string
Questions []string
}{
j.Client["IP"],
shortenResourceRecords(j.Extras),
shortenResourceRecords(j.Answers),
shortenResourceRecords(j.Nameservers),
shortenResourceRecords(j.Questions),
})
}
func (p *DNSProxy) onRequestFilter(query *dns.Msg, clientIP string) (req, res *dns.Msg) {
p.Debug("< %s %s", clientIP, query.Question)
// do we have a proxy script?
if p.Script == nil {
return query, nil
}
// run the module OnRequest callback if defined
jsreq, jsres := p.Script.OnRequest(query, clientIP)
if jsreq != nil {
// the request has been changed by the script
p.logRequestAction(jsreq)
return jsreq.ToQuery(), nil
} else if jsres != nil {
// a fake response has been returned by the script
p.logResponseAction(jsres)
return query, jsres.ToQuery()
}
return query, nil
}
func (p *DNSProxy) onResponseFilter(req, res *dns.Msg, clientIP string) *dns.Msg {
// sometimes it happens ¯\_(ツ)_/¯
if res == nil {
return nil
}
p.Debug("> %s %s", clientIP, res.Answer)
// do we have a proxy script?
if p.Script != nil {
_, jsres := p.Script.OnResponse(req, res, clientIP)
if jsres != nil {
// the response has been changed by the script
p.logResponseAction(jsres)
return jsres.ToQuery()
}
}
return res
}

View file

@ -0,0 +1,231 @@
package dns_proxy
import (
"fmt"
"regexp"
"strings"
"github.com/bettercap/bettercap/v2/log"
"github.com/bettercap/bettercap/v2/session"
"github.com/miekg/dns"
)
var whiteSpaceRegexp = regexp.MustCompile(`\s+`)
var stripWhiteSpaceRegexp = regexp.MustCompile(`^\s*(.*?)\s*$`)
type JSQuery struct {
Answers []string
Client map[string]string
Compress bool `json:"-"`
Extras []string
Header *JSQueryHeader
Nameservers []string
Questions []string
refHash string
}
type JSQueryHeader struct {
AuthenticatedData bool
Authoritative bool
CheckingDisabled bool
Id uint16
Opcode int
Rcode int
RecursionAvailable bool
RecursionDesired bool
Response bool
Truncated bool
Zero bool
}
func (j *JSQuery) NewHash() string {
headerHash := fmt.Sprintf("%t.%t.%t.%d.%d.%d.%t.%t.%t.%t.%t",
j.Header.AuthenticatedData,
j.Header.Authoritative,
j.Header.CheckingDisabled,
j.Header.Id,
j.Header.Opcode,
j.Header.Rcode,
j.Header.RecursionAvailable,
j.Header.RecursionDesired,
j.Header.Response,
j.Header.Truncated,
j.Header.Zero)
hash := fmt.Sprintf("%s.%s.%t.%s.%s.%s.%s",
strings.Join(j.Answers, ""),
j.Client["IP"],
j.Compress,
strings.Join(j.Extras, ""),
headerHash,
strings.Join(j.Nameservers, ""),
strings.Join(j.Questions, ""))
return hash
}
func NewJSQuery(query *dns.Msg, clientIP string) *JSQuery {
answers := []string{}
extras := []string{}
nameservers := []string{}
questions := []string{}
header := &JSQueryHeader{
AuthenticatedData: query.MsgHdr.AuthenticatedData,
Authoritative: query.MsgHdr.Authoritative,
CheckingDisabled: query.MsgHdr.CheckingDisabled,
Id: query.MsgHdr.Id,
Opcode: query.MsgHdr.Opcode,
Rcode: query.MsgHdr.Rcode,
RecursionAvailable: query.MsgHdr.RecursionAvailable,
RecursionDesired: query.MsgHdr.RecursionDesired,
Response: query.MsgHdr.Response,
Truncated: query.MsgHdr.Truncated,
Zero: query.MsgHdr.Zero,
}
for _, rr := range query.Answer {
answers = append(answers, rr.String())
}
for _, rr := range query.Extra {
extras = append(extras, rr.String())
}
for _, rr := range query.Ns {
nameservers = append(nameservers, rr.String())
}
for _, q := range query.Question {
qType := dns.Type(q.Qtype).String()
qClass := dns.Class(q.Qclass).String()
questions = append(questions, fmt.Sprintf("%s\t%s\t%s", q.Name, qClass, qType))
}
clientMAC := ""
clientAlias := ""
if endpoint := session.I.Lan.GetByIp(clientIP); endpoint != nil {
clientMAC = endpoint.HwAddress
clientAlias = endpoint.Alias
}
client := map[string]string{"IP": clientIP, "MAC": clientMAC, "Alias": clientAlias}
jsquery := &JSQuery{
Answers: answers,
Client: client,
Compress: query.Compress,
Extras: extras,
Header: header,
Nameservers: nameservers,
Questions: questions,
}
jsquery.UpdateHash()
return jsquery
}
func stringToClass(s string) (uint16, error) {
for i, dnsClass := range dns.ClassToString {
if s == dnsClass {
return i, nil
}
}
return 0, fmt.Errorf("unkown DNS class (got %s)", s)
}
func stringToType(s string) (uint16, error) {
for i, dnsType := range dns.TypeToString {
if s == dnsType {
return i, nil
}
}
return 0, fmt.Errorf("unkown DNS type (got %s)", s)
}
func (j *JSQuery) ToQuery() *dns.Msg {
var answers []dns.RR
var extras []dns.RR
var nameservers []dns.RR
var questions []dns.Question
for _, s := range j.Answers {
rr, err := dns.NewRR(s)
if err != nil {
log.Error("error parsing DNS answer resource record: %s", err.Error())
return nil
} else {
answers = append(answers, rr)
}
}
for _, s := range j.Extras {
rr, err := dns.NewRR(s)
if err != nil {
log.Error("error parsing DNS extra resource record: %s", err.Error())
return nil
} else {
extras = append(extras, rr)
}
}
for _, s := range j.Nameservers {
rr, err := dns.NewRR(s)
if err != nil {
log.Error("error parsing DNS nameserver resource record: %s", err.Error())
return nil
} else {
nameservers = append(nameservers, rr)
}
}
for _, s := range j.Questions {
qStripped := stripWhiteSpaceRegexp.FindStringSubmatch(s)
qParts := whiteSpaceRegexp.Split(qStripped[1], -1)
if len(qParts) != 3 {
log.Error("invalid DNS question format: (got %s)", s)
return nil
}
qName := dns.Fqdn(qParts[0])
qClass, err := stringToClass(qParts[1])
if err != nil {
log.Error("error parsing DNS question class: %s", err.Error())
return nil
}
qType, err := stringToType(qParts[2])
if err != nil {
log.Error("error parsing DNS question type: %s", err.Error())
return nil
}
questions = append(questions, dns.Question{qName, qType, qClass})
}
query := &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: j.Header.Id,
Response: j.Header.Response,
Opcode: j.Header.Opcode,
Authoritative: j.Header.Authoritative,
Truncated: j.Header.Truncated,
RecursionDesired: j.Header.RecursionDesired,
RecursionAvailable: j.Header.RecursionAvailable,
Zero: j.Header.Zero,
AuthenticatedData: j.Header.AuthenticatedData,
CheckingDisabled: j.Header.CheckingDisabled,
Rcode: j.Header.Rcode,
},
Compress: j.Compress,
Question: questions,
Answer: answers,
Ns: nameservers,
Extra: extras,
}
return query
}
func (j *JSQuery) UpdateHash() {
j.refHash = j.NewHash()
}
func (j *JSQuery) WasModified() bool {
// check if any of the fields has been changed
return j.NewHash() != j.refHash
}

View file

@ -0,0 +1,99 @@
package dns_proxy
import (
"github.com/bettercap/bettercap/v2/log"
"github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/plugin"
"github.com/miekg/dns"
"github.com/robertkrimen/otto"
)
type DnsProxyScript struct {
*plugin.Plugin
doOnRequest bool
doOnResponse bool
doOnCommand bool
}
func LoadDnsProxyScript(path string, sess *session.Session) (err error, s *DnsProxyScript) {
log.Debug("loading proxy script %s ...", path)
plug, err := plugin.Load(path)
if err != nil {
return
}
// define session pointer
if err = plug.Set("env", sess.Env.Data); err != nil {
log.Error("Error while defining environment: %+v", err)
return
}
// run onLoad if defined
if plug.HasFunc("onLoad") {
if _, err = plug.Call("onLoad"); err != nil {
log.Error("Error while executing onLoad callback: %s", "\nTraceback:\n "+err.(*otto.Error).String())
return
}
}
s = &DnsProxyScript{
Plugin: plug,
doOnRequest: plug.HasFunc("onRequest"),
doOnResponse: plug.HasFunc("onResponse"),
doOnCommand: plug.HasFunc("onCommand"),
}
return
}
func (s *DnsProxyScript) OnRequest(req *dns.Msg, clientIP string) (jsreq, jsres *JSQuery) {
if s.doOnRequest {
jsreq := NewJSQuery(req, clientIP)
jsres := NewJSQuery(req, clientIP)
if _, err := s.Call("onRequest", jsreq, jsres); err != nil {
log.Error("%s", err)
return nil, nil
} else if jsreq.WasModified() {
jsreq.UpdateHash()
return jsreq, nil
} else if jsres.WasModified() {
jsres.UpdateHash()
return nil, jsres
}
}
return nil, nil
}
func (s *DnsProxyScript) OnResponse(req, res *dns.Msg, clientIP string) (jsreq, jsres *JSQuery) {
if s.doOnResponse {
jsreq := NewJSQuery(req, clientIP)
jsres := NewJSQuery(res, clientIP)
if _, err := s.Call("onResponse", jsreq, jsres); err != nil {
log.Error("%s", err)
return nil, nil
} else if jsres.WasModified() {
jsres.UpdateHash()
return nil, jsres
}
}
return nil, nil
}
func (s *DnsProxyScript) OnCommand(cmd string) bool {
if s.doOnCommand {
if ret, err := s.Call("onCommand", cmd); err != nil {
log.Error("Error while executing onCommand callback: %+v", err)
return false
} else if v, ok := ret.(bool); ok {
return v
}
}
return false
}

View file

@ -9,6 +9,7 @@ import (
"github.com/bettercap/bettercap/v2/modules/can" "github.com/bettercap/bettercap/v2/modules/can"
"github.com/bettercap/bettercap/v2/modules/caplets" "github.com/bettercap/bettercap/v2/modules/caplets"
"github.com/bettercap/bettercap/v2/modules/dhcp6_spoof" "github.com/bettercap/bettercap/v2/modules/dhcp6_spoof"
"github.com/bettercap/bettercap/v2/modules/dns_proxy"
"github.com/bettercap/bettercap/v2/modules/dns_spoof" "github.com/bettercap/bettercap/v2/modules/dns_spoof"
"github.com/bettercap/bettercap/v2/modules/events_stream" "github.com/bettercap/bettercap/v2/modules/events_stream"
"github.com/bettercap/bettercap/v2/modules/gps" "github.com/bettercap/bettercap/v2/modules/gps"
@ -45,6 +46,7 @@ func LoadModules(sess *session.Session) {
sess.Register(can.NewCanModule(sess)) sess.Register(can.NewCanModule(sess))
sess.Register(dhcp6_spoof.NewDHCP6Spoofer(sess)) sess.Register(dhcp6_spoof.NewDHCP6Spoofer(sess))
sess.Register(net_recon.NewDiscovery(sess)) sess.Register(net_recon.NewDiscovery(sess))
sess.Register(dns_proxy.NewDnsProxy(sess))
sess.Register(dns_spoof.NewDNSSpoofer(sess)) sess.Register(dns_spoof.NewDNSSpoofer(sess))
sess.Register(events_stream.NewEventsStream(sess)) sess.Register(events_stream.NewEventsStream(sess))
sess.Register(gps.NewGPS(sess)) sess.Register(gps.NewGPS(sess))