Skip to content
Merged
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,8 @@ $ interactsh-server -d oast.pro -ip 192.0.2.1,2001:db8::1

The server will automatically detect and categorize IPv4 and IPv6 addresses, returning appropriate DNS records based on the query type.

When the selected server publishes no AAAA records, the client prints a warning so that interactions from IPv6-only sources are not silently missed and mistaken for the absence of a vulnerability.

<table>
<td>

Expand Down Expand Up @@ -644,7 +646,7 @@ interactsh-server -d hackwithautomation.com -http-index banner.html

Interactsh http server optionally enables file hosting to help in security testing. This capability can be used with a self-hosted server to serve files for common payloads for **XSS, XXE, RCE** and other attacks.

To use this feature, `-http-directory` flag can be used which accepts diretory as input and files are served under `/s/` directory.
To use this feature, `-http-directory` flag can be used which accepts directory as input and files are served under `/s/` directory.

```bash
interactsh-server -d hackwithautomation.com -http-directory ./paylods
Expand Down
14 changes: 14 additions & 0 deletions cmd/interactsh-client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
Expand Down Expand Up @@ -177,6 +178,8 @@ func main() {
gologger.Info().Msgf("%s\n", interactshURL)
}

warnIfServerLacksIPv6(client)

if cliOptions.StorePayload && cliOptions.StorePayloadFile != "" {
if err := os.WriteFile(cliOptions.StorePayloadFile, []byte(strings.Join(interactshURLs, "\n")), 0644); err != nil {
gologger.Fatal().Msgf("Could not write to payload output file: %s\n", err)
Expand Down Expand Up @@ -298,6 +301,17 @@ func main() {
}
}

// warnIfServerLacksIPv6 alerts the user when the chosen server publishes no
// AAAA records, since interactions reaching the target over IPv6 would
// otherwise be dropped silently and read as "no interaction" (issue #1391).
func warnIfServerLacksIPv6(c *client.Client) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if ok, err := c.ServerSupportsIPv6(ctx); err == nil && !ok {
gologger.Warning().Msgf("Server %s publishes no IPv6 (AAAA) records; interactions from IPv6-only sources will be missed\n", c.ServerURL())
}
}

func generatePayloadURL(numberOfPayloads int, client *client.Client) []string {
interactshURLs := make([]string, numberOfPayloads)
for i := 0; i < numberOfPayloads; i++ {
Expand Down
58 changes: 58 additions & 0 deletions pkg/client/ipv6.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package client

import (
"context"
"errors"
"net"

iputil "github.com/projectdiscovery/utils/ip"
)

// errServerNotResolvable is returned when the IPv6 capability of the server
// cannot be determined because it is addressed by a literal IP rather than a
// resolvable hostname.
var errServerNotResolvable = errors.New("server is not a resolvable hostname")

// ipResolver resolves a host to its IP addresses. *net.Resolver satisfies it.
type ipResolver interface {
LookupIP(ctx context.Context, network, host string) ([]net.IP, error)
}

// hostHasIPv6 reports whether host resolves to at least one IPv6 address. A
// false result with a nil error means the host resolves but publishes no AAAA
// records; a non-nil error means resolution failed and the result is unknown.
func hostHasIPv6(ctx context.Context, resolver ipResolver, host string) (bool, error) {
addrs, err := resolver.LookupIP(ctx, "ip", host)
if err != nil {
return false, err
}
for _, addr := range addrs {
if addr.To4() == nil && addr.To16() != nil {
return true, nil
}
}
return false, nil
}

// ServerSupportsIPv6 reports whether the interactsh server in use publishes
// IPv6 (AAAA) records. A false result means interactions reaching the target
// over IPv6 are silently dropped by the server. The boolean is meaningful only
// when the returned error is nil.
func (c *Client) ServerSupportsIPv6(ctx context.Context) (bool, error) {
if c.serverURL == nil {
return false, errServerNotResolvable
}
host := c.serverURL.Hostname()
if host == "" || iputil.IsIP(host) {
return false, errServerNotResolvable
}
return hostHasIPv6(ctx, net.DefaultResolver, host)
}

// ServerURL returns the interactsh server the client registered to.
func (c *Client) ServerURL() string {
if c.serverURL == nil {
return ""
}
return c.serverURL.String()
}
39 changes: 39 additions & 0 deletions pkg/client/ipv6_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package client

import (
"context"
"errors"
"net"
"testing"

"github.com/stretchr/testify/require"
)

type stubResolver struct {
addrs []net.IP
err error
}

func (s stubResolver) LookupIP(context.Context, string, string) ([]net.IP, error) {
return s.addrs, s.err
}

func TestHostHasIPv6(t *testing.T) {
t.Run("ipv4 only reports no ipv6", func(t *testing.T) {
ok, err := hostHasIPv6(context.Background(), stubResolver{addrs: []net.IP{net.ParseIP("178.128.212.209")}}, "oast.pro")
require.NoError(t, err)
require.False(t, ok)
})

t.Run("dual stack reports ipv6", func(t *testing.T) {
ok, err := hostHasIPv6(context.Background(), stubResolver{addrs: []net.IP{net.ParseIP("178.128.212.209"), net.ParseIP("2001:db8::1")}}, "oast.pro")
require.NoError(t, err)
require.True(t, ok)
})

t.Run("resolution failure surfaces error", func(t *testing.T) {
ok, err := hostHasIPv6(context.Background(), stubResolver{err: errors.New("no such host")}, "oast.pro")
require.Error(t, err)
require.False(t, ok)
})
}
2 changes: 1 addition & 1 deletion pkg/server/http_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func TestWildcardHTTPSRequestWithPortIsStoredAsRootTLDInteraction(t *testing.T)
require.Len(t, interactions, 1)

var interaction Interaction
require.NoError(t, jsoniter.UnmarshalFromString(interactions[0], &interaction))
require.NoError(t, json.Unmarshal([]byte(interactions[0]), &interaction))
require.Equal(t, "https", interaction.Protocol)
require.Equal(t, "abc123.example.com:443", interaction.UniqueID)
require.Equal(t, "abc123.example.com:443", interaction.FullId)
Expand Down
Loading