Skip to content

Commit 58de789

Browse files
committed
fix(client): warn when server lacks IPv6 (AAAA) records
1 parent 4555c5b commit 58de789

4 files changed

Lines changed: 114 additions & 1 deletion

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,8 @@ $ interactsh-server -d oast.pro -ip 192.0.2.1,2001:db8::1
536536
537537
The server will automatically detect and categorize IPv4 and IPv6 addresses, returning appropriate DNS records based on the query type.
538538
539+
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.
540+
539541
<table>
540542
<td>
541543
@@ -644,7 +646,7 @@ interactsh-server -d hackwithautomation.com -http-index banner.html
644646
645647
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.
646648
647-
To use this feature, `-http-directory` flag can be used which accepts diretory as input and files are served under `/s/` directory.
649+
To use this feature, `-http-directory` flag can be used which accepts directory as input and files are served under `/s/` directory.
648650
649651
```bash
650652
interactsh-server -d hackwithautomation.com -http-directory ./paylods

cmd/interactsh-client/main.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"bytes"
5+
"context"
56
"encoding/json"
67
"fmt"
78
"os"
@@ -177,6 +178,8 @@ func main() {
177178
gologger.Info().Msgf("%s\n", interactshURL)
178179
}
179180

181+
warnIfServerLacksIPv6(client)
182+
180183
if cliOptions.StorePayload && cliOptions.StorePayloadFile != "" {
181184
if err := os.WriteFile(cliOptions.StorePayloadFile, []byte(strings.Join(interactshURLs, "\n")), 0644); err != nil {
182185
gologger.Fatal().Msgf("Could not write to payload output file: %s\n", err)
@@ -298,6 +301,17 @@ func main() {
298301
}
299302
}
300303

304+
// warnIfServerLacksIPv6 alerts the user when the chosen server publishes no
305+
// AAAA records, since interactions reaching the target over IPv6 would
306+
// otherwise be dropped silently and read as "no interaction" (issue #1391).
307+
func warnIfServerLacksIPv6(c *client.Client) {
308+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
309+
defer cancel()
310+
if ok, err := c.ServerSupportsIPv6(ctx); err == nil && !ok {
311+
gologger.Warning().Msgf("Server %s publishes no IPv6 (AAAA) records; interactions from IPv6-only sources will be missed\n", c.ServerURL())
312+
}
313+
}
314+
301315
func generatePayloadURL(numberOfPayloads int, client *client.Client) []string {
302316
interactshURLs := make([]string, numberOfPayloads)
303317
for i := 0; i < numberOfPayloads; i++ {

pkg/client/ipv6.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net"
7+
8+
iputil "github.com/projectdiscovery/utils/ip"
9+
)
10+
11+
// errServerNotResolvable is returned when the IPv6 capability of the server
12+
// cannot be determined because it is addressed by a literal IP rather than a
13+
// resolvable hostname.
14+
var errServerNotResolvable = errors.New("server is not a resolvable hostname")
15+
16+
// ipResolver resolves a host to its IP addresses. *net.Resolver satisfies it.
17+
type ipResolver interface {
18+
LookupIP(ctx context.Context, network, host string) ([]net.IP, error)
19+
}
20+
21+
// hostHasIPv6 reports whether host resolves to at least one IPv6 address. A
22+
// false result with a nil error means the host resolves but publishes no AAAA
23+
// records; a non-nil error means resolution failed and the result is unknown.
24+
func hostHasIPv6(ctx context.Context, resolver ipResolver, host string) (bool, error) {
25+
addrs, err := resolver.LookupIP(ctx, "ip", host)
26+
if err != nil {
27+
return false, err
28+
}
29+
for _, addr := range addrs {
30+
if addr.To4() == nil && addr.To16() != nil {
31+
return true, nil
32+
}
33+
}
34+
return false, nil
35+
}
36+
37+
// ServerSupportsIPv6 reports whether the interactsh server in use publishes
38+
// IPv6 (AAAA) records. A false result means interactions reaching the target
39+
// over IPv6 are silently dropped by the server. The boolean is meaningful only
40+
// when the returned error is nil.
41+
func (c *Client) ServerSupportsIPv6(ctx context.Context) (bool, error) {
42+
if c.serverURL == nil {
43+
return false, errServerNotResolvable
44+
}
45+
host := c.serverURL.Hostname()
46+
if host == "" || iputil.IsIP(host) {
47+
return false, errServerNotResolvable
48+
}
49+
return hostHasIPv6(ctx, net.DefaultResolver, host)
50+
}
51+
52+
// ServerURL returns the interactsh server the client registered to.
53+
func (c *Client) ServerURL() string {
54+
if c.serverURL == nil {
55+
return ""
56+
}
57+
return c.serverURL.String()
58+
}

pkg/client/ipv6_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
type stubResolver struct {
13+
addrs []net.IP
14+
err error
15+
}
16+
17+
func (s stubResolver) LookupIP(context.Context, string, string) ([]net.IP, error) {
18+
return s.addrs, s.err
19+
}
20+
21+
func TestHostHasIPv6(t *testing.T) {
22+
t.Run("ipv4 only reports no ipv6", func(t *testing.T) {
23+
ok, err := hostHasIPv6(context.Background(), stubResolver{addrs: []net.IP{net.ParseIP("178.128.212.209")}}, "oast.pro")
24+
require.NoError(t, err)
25+
require.False(t, ok)
26+
})
27+
28+
t.Run("dual stack reports ipv6", func(t *testing.T) {
29+
ok, err := hostHasIPv6(context.Background(), stubResolver{addrs: []net.IP{net.ParseIP("178.128.212.209"), net.ParseIP("2001:db8::1")}}, "oast.pro")
30+
require.NoError(t, err)
31+
require.True(t, ok)
32+
})
33+
34+
t.Run("resolution failure surfaces error", func(t *testing.T) {
35+
ok, err := hostHasIPv6(context.Background(), stubResolver{err: errors.New("no such host")}, "oast.pro")
36+
require.Error(t, err)
37+
require.False(t, ok)
38+
})
39+
}

0 commit comments

Comments
 (0)