diff --git a/README.md b/README.md index 44807a27..95ab6587 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ # Features - DNS/HTTP(S)/SMTP(S)/LDAP Interaction +- IPv4 and IPv6 support - CLI / Web / Burp / ZAP / Docker client - AES encryption with zero logging - Automatic ACME based Wildcard TLS w/ Auto Renewal @@ -118,7 +119,7 @@ You can configure your PDCP_API_KEY in two ways: This will generate a unique payload that can be used for OOB testing with minimal interaction information in the output. ```console -interactsh-client +$ interactsh-client _ __ __ __ (_)___ / /____ _________ ______/ /______/ /_ @@ -144,7 +145,7 @@ interactsh-client `interactsh-client` with `-sf, -session-file` flag can be used store/read the current session information from user defined file which is useful to resume the same session to poll the interactions even after the client gets stopped or closed. ```console -interactsh-client -sf interact.session +$ interactsh-client -sf interact.session _ __ __ __ (_)___ / /____ _________ ______/ /______/ /_ @@ -171,7 +172,7 @@ interactsh-client -sf interact.session Running the `interactsh-client` in **verbose mode** (v) to see the whole request and response, along with an output file to analyze afterwards. ```console -interactsh-client -v -o interactsh-logs.txt +$ interactsh-client -v -o interactsh-logs.txt _ __ __ __ (_)___ / /____ _________ ______/ /______/ /_ @@ -262,7 +263,7 @@ docker run projectdiscovery/interactsh-client:latest ``` ```console -docker run projectdiscovery/interactsh-client:latest +$ docker run projectdiscovery/interactsh-client:latest _ __ __ __ (_)___ / /____ _________ ______/ /______/ /_ @@ -341,7 +342,7 @@ Usage: Flags: INPUT: -d, -domain string[] single/multiple configured domain to use for server - -ip string public ip address to use for interactsh server + -ip string[] public ip address(es) to use for interactsh server (comma-separated,supports both IPv4 & IPv6) -lip, -listen-ip string public ip address to listen on (default "0.0.0.0") -e, -eviction int number of days to persist interaction data in memory (default 30) -ne, -no-eviction disable periodic data eviction from memory @@ -448,7 +449,7 @@ A number of needed flags are configured automatically to run `interactsh-server` ## Running Interactsh Server ```console -interactsh-server -domain interact.sh +$ interactsh-server -domain interact.sh _ __ __ __ (_)___ / /____ _________ ______/ /______/ /_ @@ -473,7 +474,7 @@ interactsh-server -domain interact.sh Multiple domain names can be given in the same way as above to run the same interactsh server across multiple **configured domains**. ```console -interactsh-server -d oast.pro,oast.me +$ interactsh-server -d oast.pro,oast.me _ __ __ __ (_)___ / /____ _________ ______/ /______/ /_ @@ -495,6 +496,34 @@ interactsh-server -d oast.pro,oast.me [DNS] Listening on UDP 46.101.25.250:53 ``` +## Interactsh Server with IPv4 and IPv6 + +Interactsh server supports both IPv4 and IPv6 addresses. You can specify multiple IP addresses using the `-ip` flag, and the server will respond with the appropriate A (IPv4) or AAAA (IPv6) records in DNS responses. + +```console +$ interactsh-server -d oast.pro -ip 192.0.2.1,2001:db8::1 + + _ __ __ __ + (_)___ / /____ _________ ______/ /______/ /_ + / / __ \/ __/ _ \/ ___/ __ '/ ___/ __/ ___/ __ \ + / / / / / /_/ __/ / / /_/ / /__/ /_(__ ) / / / +/_/_/ /_/\__/\___/_/ \__,_/\___/\__/____/_/ /_/ 1.0.5 + + projectdiscovery.io + +[INF] Configured IP addresses: 192.0.2.1, 2001:db8::1 +[INF] Listening with the following services: +[HTTPS] Listening on TCP 46.101.25.250:443 +[HTTP] Listening on TCP 46.101.25.250:80 +[SMTPS] Listening on TCP 46.101.25.250:587 +[LDAP] Listening on TCP 46.101.25.250:389 +[SMTP] Listening on TCP 46.101.25.250:25 +[DNS] Listening on TCP 46.101.25.250:53 +[DNS] Listening on UDP 46.101.25.250:53 +``` + +The server will automatically detect and categorize IPv4 and IPv6 addresses, returning appropriate DNS records based on the query type. +
@@ -564,7 +593,7 @@ stream { **Configured Domains** ```console -interactsh-server -d oast.pro,oast.me +$ interactsh-server -d oast.pro,oast.me _ __ __ __ (_)___ / /____ _________ ______/ /______/ /_ @@ -590,7 +619,7 @@ interactsh-server -d oast.pro,oast.me Index page for http server can be customized while running custom interactsh server using `-http-index` flag. -```console +```bash interactsh-server -d hackwithautomation.com -http-index banner.html ``` @@ -605,7 +634,7 @@ Interactsh http server optionally enables file hosting to help in security testi To use this feature, `-http-directory` flag can be used which accepts diretory as input and files are served under `/s/` directory. -```console +```bash interactsh-server -d hackwithautomation.com -http-directory ./paylods ``` @@ -623,7 +652,7 @@ The following query parameter names are supported - `body`, `header`, `status` a - **delay** (response time) ```console -curl -i 'https://hackwithautomation.com/x?status=307&body=this+is+example+body&delay=1&header=header1:value1&header=header1:value12' +$ curl -i 'https://hackwithautomation.com/x?status=307&body=this+is+example+body&delay=1&header=header1:value1&header=header1:value12' HTTP/2 307 header1: value1 @@ -648,7 +677,7 @@ this is example body To enable `wildcard` interaction for configured Interactsh domain `wildcard` flag can be used with implicit authentication protection via the `auth` flag if the `token` flag is omitted. ```console -interactsh-server -domain hackwithautomation.com -wildcard +$ interactsh-server -domain hackwithautomation.com -wildcard _ __ __ __ (_)___ / /____ _________ ______/ /______/ /_ @@ -674,7 +703,7 @@ interactsh-server -domain hackwithautomation.com -wildcard As default, Interactsh server support LDAP interaction for the payload included in [search query](https://ldapwiki.com/wiki/LDAP%20Query%20Examples), additionally `ldap` flag can be used for complete logging. ```console -interactsh-server -domain hackwithautomation.com -sa -ldap +$ interactsh-server -domain hackwithautomation.com -sa -ldap _ __ __ __ (_)___ / /____ _________ ______/ /______/ /_ @@ -699,7 +728,7 @@ The length of the interactsh payload is **33** by default, consisting of **20** ```console -interactsh-server -d hackwithautomation.com -cidl 4 -cidn 6 +$ interactsh-server -d hackwithautomation.com -cidl 4 -cidn 6 _ __ __ __ (_)___ / /____ _________ ______/ /______/ /_ @@ -723,7 +752,7 @@ interactsh-server -d hackwithautomation.com -cidl 4 -cidn 6 **Note:** It is important and required to use same length on both side (**client** and **server**), otherwise co-relation will not work. ```console -interactsh-client -s hackwithautomation.com -cidl 4 -cidn 6 +$ interactsh-client -s hackwithautomation.com -cidl 4 -cidn 6 _ __ __ __ (_)___ / /____ _________ ______/ /______/ /_ @@ -745,7 +774,7 @@ The [certmagic](https://github.com/caddyserver/certmagic) library is used by def ```console -interactsh-server -d hackwithautomation.com -cert hackwithautomation.com.crt -privkey hackwithautomation.com.key +$ interactsh-server -d hackwithautomation.com -cert hackwithautomation.com.crt -privkey hackwithautomation.com.key _ __ __ __ (_)___ / /____ _________ ______/ /______/ /_ @@ -802,14 +831,14 @@ $ sudo interactsh-server -smb -skip-acme -debug -domain localhost ### Responder [Responder](https://github.com/lgandx/Responder) is wrapped in a docker container exposing various service ports via docker port forwarding. The interactions are retrieved by monitoring the shared log file `Responder-Session.log` in the temp folder. To use it on a self-hosted instance, it's necessary first to build the docker container and tag it as `interactsh`(docker daemon must be configured correctly and with port forwarding capabilities): -```console +```bash docker build . -t interactsh ``` Then run the service with: -```console -$ sudo interactsh-server -responder -d localhost +```bash +sudo interactsh-server -responder -d localhost ``` On default settings, the daemon listens on the following ports: diff --git a/cmd/interactsh-server/main.go b/cmd/interactsh-server/main.go index d329499e..49825781 100644 --- a/cmd/interactsh-server/main.go +++ b/cmd/interactsh-server/main.go @@ -11,6 +11,7 @@ import ( "os" "os/signal" "path/filepath" + "strconv" "strings" "time" @@ -44,7 +45,7 @@ func main() { flagSet.CreateGroup("input", "Input", flagSet.StringSliceVarP(&cliOptions.Domains, "domain", "d", []string{}, "single/multiple configured domain to use for server", goflags.CommaSeparatedStringSliceOptions), - flagSet.StringVar(&cliOptions.IPAddress, "ip", "", "public ip address to use for interactsh server"), + flagSet.StringSliceVarP(&cliOptions.IPAddresses, "ip", "i", []string{}, "public IP address(es) to use for interactsh server (comma-separated, supports both IPv4 & IPv6)", goflags.CommaSeparatedStringSliceOptions), flagSet.StringVarP(&cliOptions.ListenIP, "listen-ip", "lip", "0.0.0.0", "public ip address to listen on"), flagSet.IntVarP(&cliOptions.Eviction, "eviction", "e", 30, "number of days to persist interaction data in memory"), flagSet.BoolVarP(&cliOptions.NoEviction, "no-eviction", "ne", false, "disable periodic data eviction from memory"), @@ -149,7 +150,7 @@ func main() { gologger.Fatal().Msgf("CorrelationIdNonceLength (cidn) must be at least %d\n", settings.CorrelationIdNonceLengthMinimum) } - if cliOptions.IPAddress == "" && cliOptions.ListenIP == "0.0.0.0" { + if len(cliOptions.IPAddresses) == 0 && cliOptions.ListenIP == "0.0.0.0" { publicIP, _ := getPublicIP() outboundIP, _ := iputil.GetSourceIP("scanme.sh") @@ -179,7 +180,11 @@ func main() { gologger.Fatal().Msgf("%s\nNo bindable address could be found for port %d\nPlease ensure to have proper privileges and/or choose the correct ip:\n%s\n", err, cliOptions.DnsPort, addressesBuilder.String()) } cliOptions.ListenIP = bindableIP - cliOptions.IPAddress = publicIP + cliOptions.IPAddresses = append(cliOptions.IPAddresses, publicIP) + } + + if len(cliOptions.IPAddresses) > 0 { + gologger.Info().Msgf("Configured IP addresses: %s\n", strings.Join(cliOptions.IPAddresses, ", ")) } for _, domain := range cliOptions.Domains { @@ -424,8 +429,9 @@ func main() { network = "TCP" port = serverOptions.LdapPort } + address := net.JoinHostPort(serverOptions.ListenIP, strconv.Itoa(port)) if status { - gologger.Silent().Msgf("[%s] Listening on %s %s:%d", service, network, serverOptions.ListenIP, port) + gologger.Silent().Msgf("[%s] Listening on %s %s", service, network, address) } else if fatal { gologger.Fatal().Msgf("The %s %s service has unexpectedly stopped", network, service) } else { diff --git a/pkg/options/server_options.go b/pkg/options/server_options.go index 3001427e..460e579f 100644 --- a/pkg/options/server_options.go +++ b/pkg/options/server_options.go @@ -1,7 +1,10 @@ package options import ( + "net" + "github.com/projectdiscovery/goflags" + "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/interactsh/pkg/server" ) @@ -12,7 +15,7 @@ type CLIServerOptions struct { Debug bool Domains goflags.StringSlice DnsPort int - IPAddress string + IPAddresses goflags.StringSlice ListenIP string HttpPort int HttpsPort int @@ -57,10 +60,25 @@ type CLIServerOptions struct { } func (cliServerOptions *CLIServerOptions) AsServerOptions() *server.Options { + var ipAddresses []net.IP + + for _, ipAddress := range cliServerOptions.IPAddresses { + parsedIP := net.ParseIP(ipAddress) + if parsedIP != nil { + ipAddresses = append(ipAddresses, parsedIP) + } else { + if cliServerOptions.Debug { + gologger.Warning().Msgf("Invalid IP address '%s' will be ignored\n", ipAddress) + } + } + } + + ipAddresses = uniqueIPs(ipAddresses) + return &server.Options{ Domains: cliServerOptions.Domains, DnsPort: cliServerOptions.DnsPort, - IPAddress: cliServerOptions.IPAddress, + IPAddresses: ipAddresses, ListenIP: cliServerOptions.ListenIP, HttpPort: cliServerOptions.HttpPort, HttpsPort: cliServerOptions.HttpsPort, @@ -95,3 +113,19 @@ func (cliServerOptions *CLIServerOptions) AsServerOptions() *server.Options { HeaderServer: cliServerOptions.HeaderServer, } } + +// uniqueIPs removes duplicate IP addresses from a slice +func uniqueIPs(ips []net.IP) []net.IP { + seen := make(map[string]bool) + result := []net.IP{} + + for _, ip := range ips { + key := ip.String() + if !seen[key] { + seen[key] = true + result = append(result, ip) + } + } + + return result +} diff --git a/pkg/server/dns_server.go b/pkg/server/dns_server.go index 71367490..21388c67 100644 --- a/pkg/server/dns_server.go +++ b/pkg/server/dns_server.go @@ -9,6 +9,7 @@ import ( "strings" "sync/atomic" "time" + "unicode" jsoniter "github.com/json-iterator/go" "github.com/miekg/dns" @@ -24,7 +25,7 @@ type DNSServer struct { options *Options mxDomains map[string]string nsDomains map[string][]string - ipAddress net.IP + ipAddresses []net.IP timeToLive uint32 server *dns.Server customRecords *customDNSRecords @@ -49,14 +50,14 @@ func NewDNSServer(network string, options *Options) *DNSServer { server := &DNSServer{ options: options, - ipAddress: net.ParseIP(options.IPAddress), + ipAddresses: options.IPAddresses, mxDomains: mxDomains, nsDomains: nsDomains, timeToLive: 3600, customRecords: newCustomDNSRecordsServer(options.CustomRecords), } server.server = &dns.Server{ - Addr: options.ListenIP + fmt.Sprintf(":%d", options.DnsPort), + Addr: formatAddress(options.ListenIP, options.DnsPort), Net: network, Handler: server, } @@ -157,24 +158,35 @@ func (h *DNSServer) handleACMETXTChallenge(zone string, m *dns.Msg) error { func (h *DNSServer) handleACNAMEANY(zone string, m *dns.Msg) { nsHeader := dns.RR_Header{Name: zone, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: h.timeToLive} - // If we have a custom record serve it, or default IP + // If we have a custom record serve it, or default IPs record := h.customRecords.checkCustomResponse(zone) - switch { - case record != "": - h.resultFunction(nsHeader, zone, net.ParseIP(record), m) - default: - h.resultFunction(nsHeader, zone, h.ipAddress, m) + answerIPs := uniqueIPs(parseIPList(record)) + if len(answerIPs) == 0 { + answerIPs = h.ipAddresses + } + if len(answerIPs) > 0 { + h.resultFunction(nsHeader, zone, answerIPs, m) } } -func (h *DNSServer) resultFunction(nsHeader dns.RR_Header, zone string, ipAddress net.IP, m *dns.Msg) { - m.Answer = append(m.Answer, &dns.A{Hdr: dns.RR_Header{Name: zone, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: h.timeToLive}, A: ipAddress}) +func (h *DNSServer) resultFunction(nsHeader dns.RR_Header, zone string, ipAddresses []net.IP, m *dns.Msg) { + unique := uniqueIPs(ipAddresses) + var answered bool + for _, ipAddress := range unique { + if h.appendAnswerRecord(zone, ipAddress, m) { + answered = true + } + } + if !answered { + return + } + dotDomains := []string{zone, dns.Fqdn(h.options.Domains[0])} for _, dotDomain := range dotDomains { if nsDomains, ok := h.nsDomains[dotDomain]; ok { for _, nsDomain := range nsDomains { m.Ns = append(m.Ns, &dns.NS{Hdr: nsHeader, Ns: nsDomain}) - m.Extra = append(m.Extra, &dns.A{Hdr: dns.RR_Header{Name: nsDomain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: h.timeToLive}, A: h.ipAddress}) + h.appendGlueRecords(nsDomain, m) } return } @@ -348,6 +360,94 @@ func (h *DNSServer) handleInteraction(domain string, w dns.ResponseWriter, r *dn } } +// appendAnswerRecord appends an A/AAAA record to the DNS message based on the +// provided IP address. +func (h *DNSServer) appendAnswerRecord(zone string, ip net.IP, m *dns.Msg) bool { + if ip == nil { + return false + } + + if ipv4 := ip.To4(); ipv4 != nil { + m.Answer = append(m.Answer, &dns.A{Hdr: dns.RR_Header{Name: zone, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: h.timeToLive}, A: ipv4}) + return true + } + + if ipv6 := ip.To16(); ipv6 != nil { + m.Answer = append(m.Answer, &dns.AAAA{Hdr: dns.RR_Header{Name: zone, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: h.timeToLive}, AAAA: ipv6}) + return true + } + + return false +} + +// appendGlueRecords appends A/AAAA glue records for the given nameserver domain. +func (h *DNSServer) appendGlueRecords(nsDomain string, m *dns.Msg) { + for _, ip := range uniqueIPs(h.ipAddresses) { + if ipv4 := ip.To4(); ipv4 != nil { + m.Extra = append(m.Extra, &dns.A{Hdr: dns.RR_Header{Name: nsDomain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: h.timeToLive}, A: ipv4}) + continue + } + + if ipv6 := ip.To16(); ipv6 != nil { + m.Extra = append(m.Extra, &dns.AAAA{Hdr: dns.RR_Header{Name: nsDomain, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: h.timeToLive}, AAAA: ipv6}) + } + } +} + +// parseIPList parses a string containing multiple IP addresses separated by +// commas, semicolons, or whitespace and returns a slice of [net.IP] objects. +func parseIPList(value string) []net.IP { + if strings.TrimSpace(value) == "" { + return nil + } + + parts := strings.FieldsFunc(value, func(r rune) bool { + return r == ',' || r == ';' || unicode.IsSpace(r) + }) + + var ips []net.IP + + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed == "" { + continue + } + + if ip := net.ParseIP(trimmed); ip != nil { + ips = append(ips, ip) + } + } + + return ips +} + +// uniqueIPs deduplicates a slice of [net.IP] objects and returns a new slice +// containing only unique IP addresses. +func uniqueIPs(ips []net.IP) []net.IP { + if len(ips) == 0 { + return nil + } + + seen := make(map[string]struct{}, len(ips)) + + var result []net.IP + for _, ip := range ips { + if ip == nil { + continue + } + + key := ip.String() + if _, ok := seen[key]; ok { + continue + } + + seen[key] = struct{}{} + result = append(result, ip) + } + + return result +} + // customDNSRecords is a server for custom dns records type customDNSRecords struct { records map[string]string diff --git a/pkg/server/dns_server_test.go b/pkg/server/dns_server_test.go new file mode 100644 index 00000000..942c3692 --- /dev/null +++ b/pkg/server/dns_server_test.go @@ -0,0 +1,202 @@ +package server + +import ( + "net" + "testing" + + "github.com/miekg/dns" + "github.com/projectdiscovery/interactsh/pkg/server/acme" + "github.com/stretchr/testify/require" +) + +func TestDNSServerReturnsAAAARecords(t *testing.T) { + opts := newTestOptions([]string{"192.0.2.1", "2001:db8::1"}, "127.0.0.1") + dnsServer := NewDNSServer("udp", opts) + + msg := new(dns.Msg) + dnsServer.handleACNAMEANY(dns.Fqdn("test.example.com"), msg) + + require.True(t, hasRecord(msg.Answer, dns.TypeA, "192.0.2.1"), "expected A record") + require.True(t, hasRecord(msg.Answer, dns.TypeAAAA, "2001:db8::1"), "expected AAAA record") + require.True(t, hasRecord(msg.Extra, dns.TypeAAAA, "2001:db8::1"), "expected AAAA glue record") +} + +func TestDNSServerIPv6OnlyResponses(t *testing.T) { + opts := newTestOptions([]string{"2001:db8::5"}, "::1") + dnsServer := NewDNSServer("udp", opts) + + msg := new(dns.Msg) + dnsServer.handleACNAMEANY(dns.Fqdn("ipv6.example.com"), msg) + + require.False(t, hasRecord(msg.Answer, dns.TypeA, ""), "did not expect IPv4 record") + require.True(t, hasRecord(msg.Answer, dns.TypeAAAA, "2001:db8::5"), "expected IPv6 answer") +} + +func TestDNSServerCustomIPv6Record(t *testing.T) { + opts := newTestOptions([]string{"192.0.2.1", "2001:db8::1"}, "127.0.0.1") + dnsServer := NewDNSServer("udp", opts) + dnsServer.customRecords.records["ipv6"] = "2001:db8::dead" + + msg := new(dns.Msg) + dnsServer.handleACNAMEANY(dns.Fqdn("ipv6.example.com"), msg) + + require.True(t, hasRecord(msg.Answer, dns.TypeAAAA, "2001:db8::dead"), "expected custom AAAA record") +} + +func TestDNSServerNoConfiguredIPs(t *testing.T) { + opts := newTestOptions(nil, "127.0.0.1") + dnsServer := NewDNSServer("udp", opts) + + msg := new(dns.Msg) + dnsServer.handleACNAMEANY(dns.Fqdn("empty.example.com"), msg) + + require.Empty(t, msg.Answer, "expected no answers when no IPs configured") + require.Empty(t, msg.Extra, "expected no glue records when no IPs configured") +} + +func TestDNSGlueRecordsIncludeBothFamilies(t *testing.T) { + opts := newTestOptions([]string{"192.0.2.1", "2001:db8::1"}, "127.0.0.1") + dnsServer := NewDNSServer("udp", opts) + + msg := new(dns.Msg) + dnsServer.handleACNAMEANY(dns.Fqdn("glue.example.com"), msg) + + require.True(t, hasRecord(msg.Extra, dns.TypeA, "192.0.2.1"), "expected IPv4 glue") + require.True(t, hasRecord(msg.Extra, dns.TypeAAAA, "2001:db8::1"), "expected IPv6 glue") +} + +func TestDNSServerDefaultIPv4Answer(t *testing.T) { + opts := newTestOptions([]string{"192.0.2.50"}, "127.0.0.1") + dnsServer := NewDNSServer("udp", opts) + + msg := new(dns.Msg) + dnsServer.handleACNAMEANY(dns.Fqdn("test.example.com"), msg) + + require.Len(t, msg.Answer, 1) + require.True(t, hasRecord(msg.Answer, dns.TypeA, "192.0.2.50")) +} + +func TestDNSServerCustomIPv4Record(t *testing.T) { + opts := newTestOptions([]string{"192.0.2.50"}, "127.0.0.1") + dnsServer := NewDNSServer("udp", opts) + dnsServer.customRecords.records["app"] = "198.51.100.5" + + msg := new(dns.Msg) + dnsServer.handleACNAMEANY(dns.Fqdn("app.example.com"), msg) + + require.True(t, hasRecord(msg.Answer, dns.TypeA, "198.51.100.5"), "expected custom IPv4 answer") + require.False(t, hasRecord(msg.Answer, dns.TypeA, "192.0.2.50"), "default address should be overridden") +} + +func TestDNSServerHandleMX(t *testing.T) { + opts := newTestOptions([]string{"192.0.2.50"}, "127.0.0.1") + dnsServer := NewDNSServer("udp", opts) + + msg := new(dns.Msg) + dnsServer.handleMX(dns.Fqdn("example.com"), msg) + + require.Len(t, msg.Answer, 1) + record, ok := msg.Answer[0].(*dns.MX) + require.True(t, ok) + require.Equal(t, "mail.example.com.", record.Mx) + require.EqualValues(t, 1, record.Preference) +} + +func TestDNSServerHandleNS(t *testing.T) { + opts := newTestOptions([]string{"192.0.2.50"}, "127.0.0.1") + dnsServer := NewDNSServer("udp", opts) + + msg := new(dns.Msg) + dnsServer.handleNS(dns.Fqdn("example.com"), msg) + + require.Len(t, msg.Answer, 2) + require.Contains(t, []string{msg.Answer[0].(*dns.NS).Ns, msg.Answer[1].(*dns.NS).Ns}, "ns1.example.com.") + require.Contains(t, []string{msg.Answer[0].(*dns.NS).Ns, msg.Answer[1].(*dns.NS).Ns}, "ns2.example.com.") +} + +func TestDNSServerHandleSOA(t *testing.T) { + opts := newTestOptions([]string{"192.0.2.50"}, "127.0.0.1") + dnsServer := NewDNSServer("udp", opts) + + msg := new(dns.Msg) + dnsServer.handleSOA(dns.Fqdn("example.com"), msg) + + require.Len(t, msg.Answer, 1) + record, ok := msg.Answer[0].(*dns.SOA) + require.True(t, ok) + require.Equal(t, "ns1.example.com.", record.Ns) + require.Equal(t, acme.CertificateAuthority, record.Mbox) +} + +func TestDNSServerHandleTXT(t *testing.T) { + opts := newTestOptions([]string{"192.0.2.50"}, "127.0.0.1") + dnsServer := NewDNSServer("udp", opts) + dnsServer.TxtRecord = "verification-string" + + msg := new(dns.Msg) + dnsServer.handleTXT(dns.Fqdn("example.com"), msg) + + require.Len(t, msg.Answer, 1) + record, ok := msg.Answer[0].(*dns.TXT) + require.True(t, ok) + require.Equal(t, []string{"verification-string"}, record.Txt) +} + +func TestDNSServerMultipleIPv4Addresses(t *testing.T) { + opts := newTestOptions([]string{"192.0.2.1", "198.51.100.1", "203.0.113.1"}, "127.0.0.1") + dnsServer := NewDNSServer("udp", opts) + + msg := new(dns.Msg) + dnsServer.handleACNAMEANY(dns.Fqdn("multi.example.com"), msg) + + require.Len(t, msg.Answer, 3) + require.True(t, hasRecord(msg.Answer, dns.TypeA, "192.0.2.1")) + require.True(t, hasRecord(msg.Answer, dns.TypeA, "198.51.100.1")) + require.True(t, hasRecord(msg.Answer, dns.TypeA, "203.0.113.1")) +} + +func TestDNSServerMixedIPv4AndIPv6(t *testing.T) { + opts := newTestOptions([]string{"192.0.2.1", "2001:db8::1", "198.51.100.1", "2001:db8::2"}, "127.0.0.1") + dnsServer := NewDNSServer("udp", opts) + + msg := new(dns.Msg) + dnsServer.handleACNAMEANY(dns.Fqdn("mixed.example.com"), msg) + + require.Len(t, msg.Answer, 4) + require.True(t, hasRecord(msg.Answer, dns.TypeA, "192.0.2.1")) + require.True(t, hasRecord(msg.Answer, dns.TypeA, "198.51.100.1")) + require.True(t, hasRecord(msg.Answer, dns.TypeAAAA, "2001:db8::1")) + require.True(t, hasRecord(msg.Answer, dns.TypeAAAA, "2001:db8::2")) +} + +func hasRecord(rrs []dns.RR, rrtype uint16, expectedValue string) bool { + for _, rr := range rrs { + switch rec := rr.(type) { + case *dns.A: + if rrtype == dns.TypeA && (expectedValue == "" || rec.A.String() == expectedValue) { + return true + } + case *dns.AAAA: + if rrtype == dns.TypeAAAA && (expectedValue == "" || rec.AAAA.String() == expectedValue) { + return true + } + } + } + return false +} + +func newTestOptions(ips []string, listenIP string) *Options { + parsed := make([]net.IP, 0, len(ips)) + for _, ip := range ips { + parsedIP := net.ParseIP(ip) + if parsedIP != nil { + parsed = append(parsed, parsedIP) + } + } + + return &Options{ + Domains: []string{"example.com"}, + IPAddresses: parsed, + ListenIP: listenIP, + } +} diff --git a/pkg/server/http_server.go b/pkg/server/http_server.go index 1b6d7a84..a99d5334 100644 --- a/pkg/server/http_server.go +++ b/pkg/server/http_server.go @@ -77,8 +77,8 @@ func NewHTTPServer(options *Options) (*HTTPServer, error) { if server.options.EnableMetrics { router.Handle("/metrics", server.corsMiddleware(server.authMiddleware(http.HandlerFunc(server.metricsHandler)))) } - server.tlsserver = http.Server{Addr: options.ListenIP + fmt.Sprintf(":%d", options.HttpsPort), Handler: router, ErrorLog: log.New(&noopLogger{}, "", 0)} - server.nontlsserver = http.Server{Addr: options.ListenIP + fmt.Sprintf(":%d", options.HttpPort), Handler: router, ErrorLog: log.New(&noopLogger{}, "", 0)} + server.tlsserver = http.Server{Addr: formatAddress(options.ListenIP, options.HttpsPort), Handler: router, ErrorLog: log.New(&noopLogger{}, "", 0)} + server.nontlsserver = http.Server{Addr: formatAddress(options.ListenIP, options.HttpPort), Handler: router, ErrorLog: log.New(&noopLogger{}, "", 0)} return server, nil } diff --git a/pkg/server/ldap_server.go b/pkg/server/ldap_server.go index ed3dbdd6..bf60add5 100644 --- a/pkg/server/ldap_server.go +++ b/pkg/server/ldap_server.go @@ -59,7 +59,7 @@ func NewLDAPServer(options *Options, withLogger bool) (*LDAPServer, error) { func (ldapServer *LDAPServer) ListenAndServe(tlsConfig *tls.Config, ldapAlive chan bool) { ldapAlive <- true ldapServer.tlsConfig = tlsConfig - if err := ldapServer.server.ListenAndServe(fmt.Sprintf("%s:%d", ldapServer.options.ListenIP, ldapServer.options.LdapPort)); err != nil { + if err := ldapServer.server.ListenAndServe(formatAddress(ldapServer.options.ListenIP, ldapServer.options.LdapPort)); err != nil { gologger.Error().Msgf("Could not serve ldap on port 10389: %s\n", err) ldapAlive <- false } diff --git a/pkg/server/responder_server.go b/pkg/server/responder_server.go index da67ce9b..f13fa88e 100644 --- a/pkg/server/responder_server.go +++ b/pkg/server/responder_server.go @@ -2,7 +2,6 @@ package server import ( "bytes" - "net" "os" "os/exec" "path/filepath" @@ -25,18 +24,13 @@ var responderMonitorList map[string]string = map[string]string{ type ResponderServer struct { options *Options LogFile string - ipAddress net.IP cmd *exec.Cmd tmpFolder string } // NewResponderServer returns a new SMB server. func NewResponderServer(options *Options) (*ResponderServer, error) { - server := &ResponderServer{ - options: options, - ipAddress: net.ParseIP(options.IPAddress), - } - return server, nil + return &ResponderServer{options: options}, nil } // ListenAndServe listens on various responder ports diff --git a/pkg/server/server.go b/pkg/server/server.go index b067ec26..df3c142e 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -2,6 +2,7 @@ package server import ( "crypto/tls" + "net" "strings" "time" @@ -37,8 +38,8 @@ type Interaction struct { type Options struct { // Domains is the list domains for the instance. Domains []string - // IPAddress is the IP address of the current server. - IPAddress string + // IPAddresses contains the IP addresses (IPv4 and/or IPv6) for the server + IPAddresses []net.IP // ListenIP is the IP address to listen servers on ListenIP string // DomainPort is the port to listen DNS servers on diff --git a/pkg/server/smb_server.go b/pkg/server/smb_server.go index 892b87a0..a6b7ef83 100644 --- a/pkg/server/smb_server.go +++ b/pkg/server/smb_server.go @@ -3,7 +3,6 @@ package server import ( "bytes" "fmt" - "net" "os" "os/exec" "strings" @@ -26,18 +25,13 @@ var smbMonitorList map[string]string = map[string]string{ type SMBServer struct { options *Options LogFile string - ipAddress net.IP cmd *exec.Cmd tmpFile string } // NewSMBServer returns a new SMB server. func NewSMBServer(options *Options) (*SMBServer, error) { - server := &SMBServer{ - options: options, - ipAddress: net.ParseIP(options.IPAddress), - } - return server, nil + return &SMBServer{options: options}, nil } // ListenAndServe listens on smb port diff --git a/pkg/server/smtp_server.go b/pkg/server/smtp_server.go index 2b89a131..4e981ef1 100644 --- a/pkg/server/smtp_server.go +++ b/pkg/server/smtp_server.go @@ -3,7 +3,6 @@ package server import ( "bytes" "crypto/tls" - "fmt" "net" "strings" "sync/atomic" @@ -34,7 +33,7 @@ func NewSMTPServer(options *Options) (*SMTPServer, error) { return true } server.smtpServer = smtpd.Server{ - Addr: fmt.Sprintf("%s:%d", options.ListenIP, options.SmtpPort), + Addr: formatAddress(options.ListenIP, options.SmtpPort), AuthHandler: authHandler, HandlerRcpt: rcptHandler, Hostname: options.Domains[0], @@ -42,7 +41,7 @@ func NewSMTPServer(options *Options) (*SMTPServer, error) { Handler: smtpd.Handler(server.defaultHandler), } server.smtpsServer = smtpd.Server{ - Addr: fmt.Sprintf("%s:%d", options.ListenIP, options.SmtpsPort), + Addr: formatAddress(options.ListenIP, options.SmtpsPort), AuthHandler: authHandler, HandlerRcpt: rcptHandler, Hostname: options.Domains[0], @@ -58,7 +57,7 @@ func (h *SMTPServer) ListenAndServe(tlsConfig *tls.Config, smtpAlive, smtpsAlive if tlsConfig == nil { return } - srv := &smtpd.Server{Addr: fmt.Sprintf("%s:%d", h.options.ListenIP, h.options.SmtpAutoTLSPort), Handler: h.defaultHandler, Appname: "interactsh", Hostname: h.options.Domains[0]} + srv := &smtpd.Server{Addr: formatAddress(h.options.ListenIP, h.options.SmtpAutoTLSPort), Handler: h.defaultHandler, Appname: "interactsh", Hostname: h.options.Domains[0]} srv.TLSConfig = tlsConfig smtpsAlive <- true diff --git a/pkg/server/util.go b/pkg/server/util.go index 2bd2abcc..172a9acb 100644 --- a/pkg/server/util.go +++ b/pkg/server/util.go @@ -1,6 +1,8 @@ package server import ( + "net" + "strconv" "strings" "github.com/asaskevich/govalidator" @@ -18,3 +20,7 @@ func (options *Options) isCorrelationID(s string) bool { } return false } + +func formatAddress(host string, port int) string { + return net.JoinHostPort(host, strconv.Itoa(port)) +}