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
21 changes: 20 additions & 1 deletion cmd/interactsh-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ func main() {
flagSet.StringVarP(&cliOptions.DefaultHTTPResponseFile, "default-http-response", "dhr", "", "file to serve for all http requests (takes priority over other options)"),
flagSet.BoolVarP(&cliOptions.DiskStorage, "disk", "ds", false, "disk based storage"),
flagSet.StringVarP(&cliOptions.DiskStoragePath, "disk-path", "dsp", "", "disk storage path"),
flagSet.StringVarP(&cliOptions.RedisURL, "redis-url", "ru", "", "redis connection URL (enables shared state for multi-instance deployments)"),
flagSet.StringVarP(&cliOptions.RedisKeyPrefix, "redis-prefix", "rp", "", "redis key prefix (default \"interactsh:\")"),
flagSet.StringVarP(&cliOptions.HeaderServer, "server-header", "csh", "", "custom value of Server header in response"),
flagSet.BoolVarP(&cliOptions.NoVersionHeader, "disable-version", "dv", false, "disable publishing interactsh version in response header"),
)
Expand Down Expand Up @@ -268,7 +270,24 @@ func main() {
}

var err error
store, err = storage.New(&storeOptions)
switch {
case cliOptions.RedisURL != "":
// Redis-backed storage shares state across multiple interactsh-server
// instances behind a load balancer. Disk/in-memory flags are ignored
// in this mode by design.
if cliOptions.DiskStorage {
gologger.Warning().Msgf("--redis-url is set; disk-storage flags will be ignored\n")
}
store, err = storage.NewRedis(&storage.RedisOptions{
URL: cliOptions.RedisURL,
KeyPrefix: cliOptions.RedisKeyPrefix,
EvictionTTL: evictionTTL,
EvictionStrategy: evictionStrategy,
MaxSharedInteractions: storeOptions.MaxSharedInteractions,
})
default:
store, err = storage.New(&storeOptions)
}
if err != nil {
gologger.Fatal().Msgf("couldn't create storage: %s\n", err)
}
Expand Down
14 changes: 14 additions & 0 deletions deploy/redis-test/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# syntax=docker/dockerfile:1
FROM golang:1.24-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -o /out/interactsh-server ./cmd/interactsh-server
RUN CGO_ENABLED=0 go build -trimpath -o /out/interactsh-client ./cmd/interactsh-client

FROM alpine:3.20
RUN apk add --no-cache curl ca-certificates
COPY --from=builder /out/interactsh-server /usr/local/bin/interactsh-server
COPY --from=builder /out/interactsh-client /usr/local/bin/interactsh-client
ENTRYPOINT ["interactsh-server"]
66 changes: 66 additions & 0 deletions deploy/redis-test/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: interactsh-redis-test

# Two interactsh-server instances pointing at the same Redis to exercise
# the multi-instance scenario from issue #1267. Both run with --skip-acme
# (TLS disabled) and authentication so the test harness can register/poll
# without dragging in real DNS or ACME.
services:
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 1s
timeout: 2s
retries: 10

ish-a:
build:
context: ../..
dockerfile: deploy/redis-test/Dockerfile
command:
- "-d=oast.local"
- "-i=127.0.0.1"
- "-sa"
- "-ru=redis://redis:6379"
- "-rp=ishtest:"
- "-dns-port=5353"
- "-smtp-port=2525"
- "-smtp-autotls-port=2526"
- "-smtps-port=2527"
- "-ldap-port=3389"
- "-ftp-port=2121"
- "-ftps-port=2122"
- "-smb-port=4445"
- "-a"
- "-t=testtoken-7e2c"
ports:
- "8080:80"
depends_on:
redis:
condition: service_healthy

ish-b:
build:
context: ../..
dockerfile: deploy/redis-test/Dockerfile
command:
- "-d=oast.local"
- "-i=127.0.0.1"
- "-sa"
- "-ru=redis://redis:6379"
- "-rp=ishtest:"
- "-dns-port=5353"
- "-smtp-port=2525"
- "-smtp-autotls-port=2526"
- "-smtps-port=2527"
- "-ldap-port=3389"
- "-ftp-port=2121"
- "-ftps-port=2122"
- "-smb-port=4445"
- "-a"
- "-t=testtoken-7e2c"
ports:
- "8081:80"
depends_on:
redis:
condition: service_healthy
117 changes: 117 additions & 0 deletions deploy/redis-test/verify/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// verify is a small smoke harness for issue #1267: it registers a client
// against interactsh-server "A", triggers an HTTP interaction directly on
// interactsh-server "B", and verifies that the polling client (still talking
// to "A") receives the interaction thanks to the shared Redis state.
//
// go run ./deploy/redis-test/verify
package main

import (
"context"
"fmt"
"net/http"
"os"
"strings"
"sync"
"time"

"github.com/projectdiscovery/interactsh/pkg/client"
"github.com/projectdiscovery/interactsh/pkg/server"
)

func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "verify FAILED: %v\n", err)
os.Exit(1)
}
fmt.Println("verify OK")
}

func run() error {
urlA := envOr("ISH_A_URL", "http://localhost:8080")
urlB := envOr("ISH_B_URL", "http://localhost:8081")
token := envOr("ISH_TOKEN", "testtoken-7e2c")

c, err := client.New(&client.Options{
ServerURL: urlA,
Token: token,
DisableHTTPFallback: false,
CorrelationIdLength: 20,
CorrelationIdNonceLength: 13,
})
if err != nil {
return fmt.Errorf("client.New: %w", err)
}
defer func() { _ = c.Close() }()

callbackURL := c.URL()
if callbackURL == "" {
return fmt.Errorf("client did not return a callback URL")
}
fmt.Printf("registered against A, callback URL = %s\n", callbackURL)

got := make(chan *server.Interaction, 1)
var once sync.Once
if err := c.StartPolling(500*time.Millisecond, func(ix *server.Interaction) {
once.Do(func() { got <- ix })
}); err != nil {
return fmt.Errorf("StartPolling: %w", err)
}

// Give the poller one tick to enter its loop before we trigger.
time.Sleep(750 * time.Millisecond)

// Trigger an HTTP interaction on instance B by curling its HTTP port
// with a Host header matching the registered subdomain. Instance B
// resolves the correlation id from the host, encrypts the request
// payload with the AES key that lives in shared Redis, and pushes the
// ciphertext to the data list. Instance A serves the poll.
probe := fmt.Sprintf("hello-from-instance-b-%d", time.Now().UnixNano())
if err := triggerHTTP(urlB, callbackURL, probe); err != nil {
return fmt.Errorf("trigger interaction on B: %w", err)
}
fmt.Printf("triggered HTTP interaction on B with probe %q\n", probe)

select {
case ix := <-got:
if ix == nil {
return fmt.Errorf("nil interaction received")
}
if !strings.Contains(ix.RawRequest, probe) {
return fmt.Errorf("interaction received but probe %q not present in RawRequest: %q",
probe, ix.RawRequest)
}
fmt.Printf("polled from A, captured B's interaction (protocol=%s, remote=%s)\n",
ix.Protocol, ix.RemoteAddress)
return nil
case <-time.After(15 * time.Second):
return fmt.Errorf("timed out waiting for interaction to propagate via Redis")
}
}

func triggerHTTP(serverURL, callbackURL, probe string) error {
host := strings.Split(strings.TrimPrefix(strings.TrimPrefix(callbackURL,
"http://"), "https://"), "/")[0]

req, err := http.NewRequestWithContext(context.Background(), http.MethodGet,
serverURL+"/"+probe, nil)
if err != nil {
return err
}
req.Host = host
req.Header.Set("X-Probe", probe)

resp, err := (&http.Client{Timeout: 5 * time.Second}).Do(req)
if err != nil {
return err
}
_ = resp.Body.Close()
return nil
}

func envOr(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ require (
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/akrylysov/pogreb v0.10.2 // indirect
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
github.com/alicebob/miniredis/v2 v2.38.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
Expand All @@ -52,6 +53,7 @@ require (
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.3.2 // indirect
github.com/charmbracelet/glamour v0.10.0 // indirect
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
Expand Down Expand Up @@ -121,6 +123,7 @@ require (
github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 // indirect
github.com/projectdiscovery/mapcidr v1.1.97 // indirect
github.com/projectdiscovery/networkpolicy v0.1.38 // indirect
github.com/redis/go-redis/v9 v9.19.0 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
Expand All @@ -142,13 +145,14 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.13 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zcalusic/sysinfo v1.1.3 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 // indirect
github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/crypto v0.46.0 // indirect
Expand Down
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/alicebob/miniredis/v2 v2.38.0 h1:nZAzCR+Lj+Vxk4ZXzm2NuKq2O33RXj1XxJ2e2uP9jiw=
github.com/alicebob/miniredis/v2 v2.38.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
Expand Down Expand Up @@ -74,6 +76,9 @@ github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKH
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
Expand Down Expand Up @@ -356,6 +361,8 @@ github.com/projectdiscovery/retryablehttp-go v1.3.11/go.mod h1:oTy1TiZcfb9SdDqek
github.com/projectdiscovery/utils v0.11.1 h1:PWj1KjIASxt8icxommH72C0TQqNOvGkcSODRkiq0SQw=
github.com/projectdiscovery/utils v0.11.1/go.mod h1:yktGrHGk2CTjNiccXovnvGrLHX9sV2bqz9nSnbA3V8M=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
Expand Down Expand Up @@ -446,6 +453,8 @@ github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zcalusic/sysinfo v1.1.3 h1:u/AVENkuoikKuIZ4sUEJ6iibpmQP6YpGD8SSMCrqAF0=
Expand Down Expand Up @@ -474,6 +483,8 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
Expand Down
5 changes: 5 additions & 0 deletions pkg/options/server_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ type CLIServerOptions struct {
OriginIPHeader string
DiskStorage bool
DiskStoragePath string
// RedisURL, when set, switches the server to a Redis-backed storage
// backend so multiple instances can share state behind a load balancer.
// Disk storage flags are ignored when RedisURL is set.
RedisURL string
RedisKeyPrefix string
EnablePprof bool
EnableMetrics bool
Verbose bool
Expand Down
Loading
Loading