├── .gitignore ├── pkg ├── storage │ ├── error.go │ ├── option.go │ ├── storage.go │ ├── types.go │ ├── util.go │ ├── storagedb_test.go │ └── storagedb.go ├── settings │ └── default.go ├── server │ ├── cpu_stats_nocgo.go │ ├── cpu_stats_all.go │ ├── util.go │ ├── server_test.go │ ├── http_server_test.go │ ├── acme │ │ ├── records_store.go │ │ └── acme_certbot.go │ ├── smb_server.go │ ├── responder_server.go │ ├── metrics.go │ ├── server.go │ ├── smtp_server.go │ ├── ftp_server.go │ ├── dns_server.go │ ├── ldap_server.go │ └── http_server.go ├── options │ ├── session-info.go │ ├── utils.go │ ├── client_options.go │ └── server_options.go ├── filewatcher │ └── filewatcher.go └── client │ └── client.go ├── SECURITY.md ├── cmd ├── interactsh-server │ ├── example-custom-records.yaml │ ├── smb_server.py │ ├── Dockerfile │ └── main.go ├── benchmark-server │ ├── load-testing │ │ └── bench.go │ └── duration-testing │ │ └── bench.go └── interactsh-client │ └── main.go ├── deploy ├── ansible.cfg ├── promtail.yml.j2 ├── inventory.yaml ├── README.md ├── grafana_agent.yaml ├── deploy.yaml └── agent.yaml.j2 ├── .github ├── release.yml ├── docker │ ├── client │ │ └── Dockerfile │ └── server │ │ └── Dockerfile ├── workflows │ ├── lint-test.yml │ ├── release-binary.yml │ ├── codeql-analysis.yml │ ├── build-test.yml │ ├── deploy.yml │ ├── dockerhub-server.yml │ └── dockerhub-client.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── issue-report.md └── dependabot.yml ├── examples └── client.go ├── LICENSE.md ├── .goreleaser.yml ├── internal └── runner │ └── healthcheck.go └── go.mod /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | 4 | cmd/interactsh-server/interactsh-server 5 | cmd/interactsh-client/interactsh-client 6 | *.exe 7 | dist/ -------------------------------------------------------------------------------- /pkg/storage/error.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import "errors" 4 | 5 | var ErrCorrelationIdNotFound = errors.New("could not get correlation-id from cache") 6 | -------------------------------------------------------------------------------- /pkg/settings/default.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | const ( 4 | CorrelationIdLengthDefault = 20 5 | CorrelationIdNonceLengthDefault = 13 6 | StorePayloadFileDefault = "interactsh_payload.txt" 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/server/cpu_stats_nocgo.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && !cgo 2 | 3 | package server 4 | 5 | import ( 6 | "errors" 7 | ) 8 | 9 | func getCPUStats() (*CpuStats, error) { 10 | return nil, errors.New("not supported") 11 | } 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | DO NOT CREATE AN ISSUE to report a security problem. Instead, please send an email to security@projectdiscovery.io, and we will acknowledge it within 3 working days. 6 | -------------------------------------------------------------------------------- /cmd/interactsh-server/example-custom-records.yaml: -------------------------------------------------------------------------------- 1 | # This is a reference custom DNS records file 2 | 3 | # The default custom records can be specified using this YAML 4 | # file using the below declaration. 5 | aws: "169.254.169.254" 6 | alibaba: "100.100.100.200" 7 | localhost: "127.0.0.1" 8 | oracle: "192.0.0.192" 9 | -------------------------------------------------------------------------------- /pkg/storage/option.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import "time" 4 | 5 | type Options struct { 6 | DbPath string 7 | EvictionTTL time.Duration 8 | MaxSize int 9 | } 10 | 11 | func (options *Options) UseDisk() bool { 12 | return options.DbPath != "" 13 | } 14 | 15 | var DefaultOptions = Options{ 16 | MaxSize: 2500000, 17 | } 18 | -------------------------------------------------------------------------------- /pkg/options/session-info.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | type SessionInfo struct { 4 | ServerURL string `yaml:"server-url"` 5 | Token string `yaml:"server-token"` 6 | PrivateKey string `yaml:"private-key"` 7 | CorrelationID string `yaml:"correlation-id"` 8 | SecretKey string `yaml:"secret-key"` 9 | PublicKey string `yaml:"pyblic-key"` 10 | } 11 | -------------------------------------------------------------------------------- /deploy/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | remote_user="root" 3 | host_key_checking = False 4 | action_warnings = False 5 | inventory=inventory.yaml 6 | private_key_file=~/.ssh/oast 7 | 8 | [privilege_escalation] 9 | become = True 10 | become_user = root 11 | become_ask_pass=False 12 | become_method=sudo 13 | 14 | [persistent_connection] 15 | command_timeout = 60 16 | 17 | [ssh_connection] 18 | retries=3 -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | categories: 6 | - title: 🎉 Features 7 | labels: 8 | - "Type: Enhancement" 9 | - title: 🐞 Bugs 10 | labels: 11 | - "Type: Bug" 12 | - title: 🔨 Maintenance 13 | labels: 14 | - "Type: Maintenance" 15 | - title: Other Changes 16 | labels: 17 | - "*" -------------------------------------------------------------------------------- /.github/docker/client/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base 2 | FROM golang:1.20.5-alpine AS builder 3 | RUN apk add --no-cache git build-base gcc musl-dev 4 | WORKDIR /app 5 | COPY . /app 6 | RUN go mod download 7 | RUN go build ./cmd/interactsh-client 8 | 9 | # Release 10 | FROM alpine:3.18.2 11 | RUN apk -U upgrade --no-cache \ 12 | && apk add --no-cache bind-tools ca-certificates 13 | COPY --from=builder /app/interactsh-client /usr/local/bin/ 14 | 15 | ENTRYPOINT ["interactsh-client"] -------------------------------------------------------------------------------- /pkg/server/cpu_stats_all.go: -------------------------------------------------------------------------------- 1 | //go:build !(darwin && !cgo) 2 | 3 | package server 4 | 5 | import ( 6 | "github.com/mackerelio/go-osstat/cpu" 7 | ) 8 | 9 | func getCPUStats() (*CpuStats, error) { 10 | cs, err := cpu.Get() 11 | if err != nil { 12 | return nil, err 13 | } 14 | cpuStats := &CpuStats{ 15 | User: cs.User, 16 | System: cs.System, 17 | Idle: cs.Idle, 18 | Nice: cs.Nice, 19 | Total: cs.Total, 20 | } 21 | return cpuStats, nil 22 | } 23 | -------------------------------------------------------------------------------- /cmd/interactsh-server/smb_server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from impacket import smbserver 3 | 4 | log_filename = "log.txt" 5 | if len(sys.argv) >= 2: 6 | log_filename = sys.argv[1] 7 | port = 445 8 | if len(sys.argv) >= 3: 9 | port = int(sys.argv[2]) 10 | 11 | server = smbserver.SimpleSMBServer(listenAddress="0.0.0.0", listenPort=port) 12 | server.setSMB2Support(True) 13 | server.addShare("interactsh", "/interactsh") 14 | server.setSMBChallenge('') 15 | server.setLogFile(log_filename) 16 | server.start() 17 | -------------------------------------------------------------------------------- /pkg/server/util.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/asaskevich/govalidator" 7 | "github.com/rs/xid" 8 | ) 9 | 10 | func (options *Options) isCorrelationID(s string) bool { 11 | if len(s) == options.GetIdLength() && govalidator.IsAlphanumeric(s) { 12 | // xid should be 12 13 | if options.CorrelationIdLength != 12 { 14 | return true 15 | } else if _, err := xid.FromString(strings.ToLower(s[:options.CorrelationIdLength])); err == nil { 16 | return true 17 | } 18 | } 19 | return false 20 | } 21 | -------------------------------------------------------------------------------- /cmd/interactsh-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine as compile 2 | WORKDIR /opt 3 | RUN apk add --no-cache git gcc musl-dev python3-dev libffi-dev openssl-dev cargo 4 | RUN python3 -m pip install virtualenv 5 | RUN virtualenv -p python venv 6 | ENV PATH="/opt/venv/bin:$PATH" 7 | RUN git clone --depth 1 https://github.com/SecureAuthCorp/impacket.git 8 | RUN python3 -m pip install impacket/ 9 | RUN git clone --depth 1 https://github.com/lgandx/Responder.git 10 | RUN python3 -m pip install netifaces 11 | ENTRYPOINT ["python3","/opt/Responder/Responder.py","-I","eth0"] 12 | -------------------------------------------------------------------------------- /pkg/server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/projectdiscovery/interactsh/pkg/settings" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestGetURLIDComponent(t *testing.T) { 11 | options := Options{CorrelationIdLength: settings.CorrelationIdLengthDefault, CorrelationIdNonceLength: settings.CorrelationIdNonceLengthDefault} 12 | random := options.getURLIDComponent("c6rj61aciaeutn2ae680cg5ugboyyyyyn.interactsh.com") 13 | require.Equal(t, "c6rj61aciaeutn2ae680cg5ugboyyyyyn", random, "could not get correct component") 14 | } 15 | -------------------------------------------------------------------------------- /pkg/storage/storage.go: -------------------------------------------------------------------------------- 1 | // storage defines a storage mechanism 2 | package storage 3 | 4 | type Storage interface { 5 | GetCacheMetrics() (*CacheMetrics, error) 6 | SetIDPublicKey(correlationID, secretKey, publicKey string) error 7 | SetID(ID string) error 8 | AddInteraction(correlationID string, data []byte) error 9 | AddInteractionWithId(id string, data []byte) error 10 | GetInteractions(correlationID, secret string) ([]string, string, error) 11 | GetInteractionsWithId(id string) ([]string, error) 12 | RemoveID(correlationID, secret string) error 13 | GetCacheItem(token string) (*CorrelationData, error) 14 | Close() error 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/lint-test.yml: -------------------------------------------------------------------------------- 1 | name: 🙏🏻 Lint Test 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '**.go' 7 | - '**.mod' 8 | workflow_dispatch: 9 | 10 | jobs: 11 | lint: 12 | name: Lint Test 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | - name: Set up Go 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version: 1.20.x 21 | - name: Run golangci-lint 22 | uses: golangci/golangci-lint-action@v3.6.0 23 | with: 24 | version: latest 25 | args: --timeout 5m 26 | working-directory: . -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | 5 | - name: Ask an question / advise on using interactsh 6 | url: https://github.com/projectdiscovery/interactsh/discussions/categories/q-a 7 | about: Ask a question or request support for using interactsh 8 | 9 | - name: Share idea / feature to discuss for interactsh 10 | url: https://github.com/projectdiscovery/interactsh/discussions/categories/ideas 11 | about: Share idea / feature to discuss for interactsh 12 | 13 | - name: Connect with PD Team (Discord) 14 | url: https://discord.gg/projectdiscovery 15 | about: Connect with PD Team for direct communication -------------------------------------------------------------------------------- /deploy/promtail.yml.j2: -------------------------------------------------------------------------------- 1 | server: 2 | http_listen_address: 0.0.0.0 3 | http_listen_port: 9080 4 | 5 | positions: 6 | filename: /tmp/positions.yaml 7 | 8 | clients: 9 | - url: {{ grafana_cloud_url }} 10 | external_labels: {"server_name" : "{{ domain_name }}"} 11 | 12 | scrape_configs: 13 | - job_name: flog_scrape 14 | docker_sd_configs: 15 | - host: unix:///var/run/docker.sock 16 | refresh_interval: 5s 17 | filters: 18 | - name: name 19 | values: ["{{ container_name }}"] 20 | relabel_configs: 21 | - source_labels: ['__meta_docker_container_name'] 22 | regex: '/(.*)' 23 | target_label: 'container' 24 | -------------------------------------------------------------------------------- /deploy/inventory.yaml: -------------------------------------------------------------------------------- 1 | interactsh: 2 | hosts: 3 | oast.site: 4 | ansible_host: 178.128.16.97 5 | ansible_user: root 6 | domain_name: "oast.site" 7 | 8 | oast.pro: 9 | ansible_host: 178.128.212.209 10 | ansible_user: root 11 | domain_name: "oast.pro" 12 | 13 | oast.live: 14 | ansible_host: 178.128.210.172 15 | ansible_user: root 16 | domain_name: "oast.live" 17 | 18 | oast.fun: 19 | ansible_host: 206.189.156.69 20 | ansible_user: root 21 | domain_name: "oast.fun" 22 | 23 | oast.online: 24 | ansible_host: 167.99.69.236 25 | ansible_user: root 26 | domain_name: "oast.online" 27 | 28 | oast.me: 29 | ansible_host: 178.128.209.14 30 | ansible_user: root 31 | domain_name: "oast.me" 32 | -------------------------------------------------------------------------------- /examples/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/projectdiscovery/interactsh/pkg/client" 9 | "github.com/projectdiscovery/interactsh/pkg/server" 10 | ) 11 | 12 | func main() { 13 | client, err := client.New(client.DefaultOptions) 14 | if err != nil { 15 | panic(err) 16 | } 17 | defer client.Close() 18 | 19 | client.StartPolling(time.Duration(1*time.Second), func(interaction *server.Interaction) { 20 | fmt.Printf("Got Interaction: %v => %v\n", interaction.Protocol, interaction.FullId) 21 | }) 22 | defer client.StopPolling() 23 | 24 | URL := client.URL() 25 | 26 | resp, err := http.Get("https://" + URL) 27 | if err != nil { 28 | panic(err) 29 | } 30 | resp.Body.Close() 31 | 32 | fmt.Printf("Got URL: %v => %v\n", URL, resp) 33 | time.Sleep(1 * time.Second) 34 | } 35 | -------------------------------------------------------------------------------- /.github/docker/server/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base 2 | FROM golang:1.20.5-alpine AS builder 3 | RUN apk add --no-cache git build-base gcc musl-dev 4 | WORKDIR /app 5 | COPY . /app 6 | RUN go mod download 7 | RUN go build ./cmd/interactsh-server 8 | 9 | 10 | # Release 11 | FROM alpine:3.18.2 12 | RUN apk -U upgrade --no-cache \ 13 | && apk add --no-cache bind-tools ca-certificates python3 libffi curl \ 14 | && apk add --no-cache --virtual .build-deps python3-dev py3-pip py3-wheel libffi-dev build-base \ 15 | && python3 -m pip install impacket \ 16 | && python3 -m pip cache purge \ 17 | && apk del .build-deps 18 | RUN curl -o /usr/local/bin/smb_server.py https://raw.githubusercontent.com/projectdiscovery/interactsh/main/cmd/interactsh-server/smb_server.py 19 | WORKDIR "/usr/local/bin" 20 | COPY --from=builder /app/interactsh-server /usr/local/bin/ 21 | 22 | ENTRYPOINT ["interactsh-server"] -------------------------------------------------------------------------------- /pkg/options/utils.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "github.com/projectdiscovery/gologger" 5 | updateutils "github.com/projectdiscovery/utils/update" 6 | ) 7 | 8 | const Version = "1.1.5" 9 | 10 | var banner = (` 11 | _ __ __ __ 12 | (_)___ / /____ _________ ______/ /______/ /_ 13 | / / __ \/ __/ _ \/ ___/ __ '/ ___/ __/ ___/ __ \ 14 | / / / / / /_/ __/ / / /_/ / /__/ /_(__ ) / / / 15 | /_/_/ /_/\__/\___/_/ \__,_/\___/\__/____/_/ /_/ 16 | `) 17 | 18 | func ShowBanner() { 19 | gologger.Print().Msgf("%s\n", banner) 20 | gologger.Print().Msgf("\t\tprojectdiscovery.io\n\n") 21 | } 22 | 23 | // GetUpdateCallback returns a callback function that updates interactsh 24 | func GetUpdateCallback(assetName string) func() { 25 | return func() { 26 | ShowBanner() 27 | updateutils.GetUpdateToolFromRepoCallback(assetName, Version, "interactsh")() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/release-binary.yml: -------------------------------------------------------------------------------- 1 | name: 🎉 Release Binary 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest-16-cores 12 | steps: 13 | - name: "Check out code" 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: "Set up Go" 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: 1.20.x 22 | - name: "Create release on GitHub" 23 | uses: goreleaser/goreleaser-action@v4 24 | with: 25 | args: "release --rm-dist" 26 | version: latest 27 | workdir: . 28 | env: 29 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 30 | SLACK_WEBHOOK: "${{ secrets.RELEASE_SLACK_WEBHOOK }}" 31 | DISCORD_WEBHOOK_ID: "${{ secrets.DISCORD_WEBHOOK_ID }}" 32 | DISCORD_WEBHOOK_TOKEN: "${{ secrets.DISCORD_WEBHOOK_TOKEN }}" -------------------------------------------------------------------------------- /pkg/options/client_options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/projectdiscovery/goflags" 7 | ) 8 | 9 | type CLIClientOptions struct { 10 | Match goflags.StringSlice 11 | Filter goflags.StringSlice 12 | Config string 13 | Version bool 14 | ServerURL string 15 | NumberOfPayloads int 16 | Output string 17 | JSON bool 18 | StorePayload bool 19 | StorePayloadFile string 20 | Verbose bool 21 | PollInterval int 22 | DNSOnly bool 23 | HTTPOnly bool 24 | SmtpOnly bool 25 | Token string 26 | DisableHTTPFallback bool 27 | CorrelationIdLength int 28 | CorrelationIdNonceLength int 29 | SessionFile string 30 | Asn bool 31 | DisableUpdateCheck bool 32 | KeepAliveInterval time.Duration 33 | } 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request feature to implement in this project 4 | title: "" 5 | labels: 'Type: Enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 16 | 17 | ### Please describe your feature request: 18 | 19 | 20 | ### Describe the use case of this feature: 21 | 22 | -------------------------------------------------------------------------------- /pkg/storage/types.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type GetInteractionsFunc func() []string 9 | 10 | type CacheMetrics struct { 11 | HitCount uint64 `json:"hit-count"` 12 | MissCount uint64 `json:"miss-count"` 13 | LoadSuccessCount uint64 `json:"load-success-count"` 14 | LoadErrorCount uint64 `json:"load-error-count"` 15 | TotalLoadTime time.Duration `json:"total-load-time"` 16 | EvictionCount uint64 `json:"eviction-count"` 17 | } 18 | 19 | // CorrelationData is the data for a correlation-id. 20 | type CorrelationData struct { 21 | sync.Mutex 22 | // data contains data for a correlation-id in AES encrypted json format. 23 | Data []string `json:"data"` 24 | // secretkey is a secret key for original user verification 25 | SecretKey string `json:"-"` 26 | // AESKey is the AES encryption key in encrypted format. 27 | AESKeyEncrypted string `json:"aes-key"` 28 | // decrypted AES key for signing 29 | AESKey []byte `json:"-"` 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 🚨 CodeQL Analysis 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - dev 8 | paths: 9 | - '**.go' 10 | - '**.mod' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'go' ] 25 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v3 30 | 31 | # Initializes the CodeQL tools for scanning. 32 | - name: Initialize CodeQL 33 | uses: github/codeql-action/init@v2 34 | with: 35 | languages: ${{ matrix.language }} 36 | 37 | - name: Autobuild 38 | uses: github/codeql-action/autobuild@v2 39 | 40 | - name: Perform CodeQL Analysis 41 | uses: github/codeql-action/analyze@v2 -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ProjectDiscovery, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: 🔨 Build Test 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '**.go' 7 | - '**.mod' 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | name: Test Builds 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, windows-latest, macOS-latest] 17 | steps: 18 | - name: Set up Go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: 1.20.x 22 | 23 | - name: Check out code 24 | uses: actions/checkout@v3 25 | 26 | - name: Build 27 | run: go build ./... 28 | working-directory: . 29 | 30 | - name: Test 31 | run: go test ./... 32 | working-directory: . 33 | 34 | # Todo 35 | # - name: Integration Tests 36 | # env: 37 | # GH_ACTION: true 38 | # run: bash run.sh 39 | # working-directory: integration_tests/ 40 | 41 | - name: Race Condition Tests 42 | run: go build -race ./... 43 | working-directory: . 44 | 45 | - name: Example Code Tests 46 | run: go build . 47 | working-directory: examples/ -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 🖥 Deploy 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | tags: 6 | description: 'Interactsh docker tag to deploy' 7 | required: true 8 | type: string 9 | jobs: 10 | build: 11 | name: Ansible Deploy 12 | runs-on: ubuntu-latest 13 | environment: oast 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Setup Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: 3.9 22 | 23 | - name: Install dependencies 24 | run: | 25 | sudo apt install ansible-core 26 | sudo -H pip install -Iv 'resolvelib<0.6.0' 27 | ansible-galaxy collection install community.docker 28 | 29 | - name: set ansible config secrets 30 | env: 31 | DO_SSH_KEY: ${{ secrets.DO_SSH_KEY }} 32 | run: | 33 | mkdir ~/.ssh 34 | echo "$DO_SSH_KEY" > ~/.ssh/oast 35 | chmod 600 ~/.ssh/oast 36 | 37 | - name: run playbook 38 | env: 39 | ANSIBLE_FORCE_COLOR: '1' 40 | run: | 41 | ansible all -m ping 42 | ansible-playbook deploy.yaml --tags deploy --extra-vars "container_tag=${{ inputs.tags }}" 43 | working-directory: ./deploy 44 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | 5 | builds: 6 | - main: cmd/interactsh-client/main.go 7 | binary: interactsh-client 8 | id: interactsh-client 9 | 10 | env: 11 | - CGO_ENABLED=0 12 | 13 | goos: [windows,linux,darwin] 14 | goarch: [amd64,386,arm,arm64] 15 | 16 | - main: cmd/interactsh-server/main.go 17 | binary: interactsh-server 18 | id: interactsh-server 19 | 20 | env: 21 | - CGO_ENABLED=0 22 | 23 | goos: [windows,linux,darwin] 24 | goarch: [amd64,386,arm,arm64] 25 | 26 | archives: 27 | - format: zip 28 | id: client 29 | builds: [interactsh-client] 30 | name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 31 | replacements: 32 | darwin: macOS 33 | 34 | - format: zip 35 | id: server 36 | builds: [interactsh-server] 37 | name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 38 | replacements: 39 | darwin: macOS 40 | 41 | checksum: 42 | algorithm: sha256 43 | 44 | announce: 45 | slack: 46 | enabled: true 47 | channel: '#release' 48 | username: GoReleaser 49 | message_template: 'New Release: {{ .ProjectName }} {{.Tag}} is published! Check it out at {{ .ReleaseURL }}' 50 | 51 | discord: 52 | enabled: true 53 | message_template: '**New Release: {{ .ProjectName }} {{.Tag}}** is published! Check it out at {{ .ReleaseURL }}' -------------------------------------------------------------------------------- /.github/workflows/dockerhub-server.yml: -------------------------------------------------------------------------------- 1 | name: 🌥 Docker Server 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["🎉 Release Binary"] 6 | types: 7 | - completed 8 | workflow_dispatch: 9 | 10 | jobs: 11 | docker: 12 | runs-on: ubuntu-latest-16-cores 13 | steps: 14 | 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Get Github tag 19 | id: meta 20 | run: | 21 | curl --silent "https://api.github.com/repos/projectdiscovery/interactsh/releases/latest" | jq -r .tag_name | xargs -I {} echo TAG={} >> $GITHUB_OUTPUT 22 | 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v2 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v2 28 | 29 | - name: Login to DockerHub 30 | uses: docker/login-action@v2 31 | with: 32 | username: ${{ secrets.DOCKER_USERNAME }} 33 | password: ${{ secrets.DOCKER_TOKEN }} 34 | 35 | - name: Build and push server 36 | uses: docker/build-push-action@v4 37 | with: 38 | context: . 39 | file: .github/docker/server/Dockerfile 40 | platforms: linux/amd64,linux/arm64 41 | push: true 42 | tags: projectdiscovery/interactsh-server:latest,projectdiscovery/interactsh-server:${{ steps.meta.outputs.TAG }} 43 | -------------------------------------------------------------------------------- /.github/workflows/dockerhub-client.yml: -------------------------------------------------------------------------------- 1 | name: 🌥 Docker Client 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["🎉 Release Binary"] 6 | types: 7 | - completed 8 | workflow_dispatch: 9 | 10 | jobs: 11 | docker: 12 | runs-on: ubuntu-latest-16-cores 13 | steps: 14 | 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Get Github tag 19 | id: meta 20 | run: | 21 | curl --silent "https://api.github.com/repos/projectdiscovery/interactsh/releases/latest" | jq -r .tag_name | xargs -I {} echo TAG={} >> $GITHUB_OUTPUT 22 | 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v2 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v2 28 | 29 | - name: Login to DockerHub 30 | uses: docker/login-action@v2 31 | with: 32 | username: ${{ secrets.DOCKER_USERNAME }} 33 | password: ${{ secrets.DOCKER_TOKEN }} 34 | 35 | - name: Build and push client 36 | uses: docker/build-push-action@v4 37 | with: 38 | context: . 39 | file: .github/docker/client/Dockerfile 40 | platforms: linux/amd64,linux/arm64,linux/arm 41 | push: true 42 | tags: projectdiscovery/interactsh-client:latest,projectdiscovery/interactsh-client:${{ steps.meta.outputs.TAG }} -------------------------------------------------------------------------------- /pkg/filewatcher/filewatcher.go: -------------------------------------------------------------------------------- 1 | package filewatcher 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "os" 7 | "sync" 8 | "time" 9 | 10 | "github.com/projectdiscovery/gologger" 11 | fileutil "github.com/projectdiscovery/utils/file" 12 | ) 13 | 14 | type Options struct { 15 | Interval time.Duration 16 | File string 17 | } 18 | 19 | type FileWatcher struct { 20 | Options Options 21 | watcher *time.Ticker 22 | } 23 | 24 | func New(options Options) (*FileWatcher, error) { 25 | return &FileWatcher{Options: options}, nil 26 | } 27 | 28 | func (f *FileWatcher) Watch() (chan string, error) { 29 | tickWatcher := time.NewTicker(f.Options.Interval) 30 | f.watcher = tickWatcher 31 | out := make(chan string) 32 | if !fileutil.FileExists(f.Options.File) { 33 | return nil, errors.New("file doesn't exist") 34 | } 35 | go func() { 36 | var seenLines sync.Map 37 | for range f.watcher.C { 38 | r, err := os.Open(f.Options.File) 39 | if err != nil { 40 | gologger.Fatal().Msgf("Couldn't monitor file: %s", err) 41 | return 42 | } 43 | sc := bufio.NewScanner(r) 44 | for sc.Scan() { 45 | data := sc.Text() 46 | _, loaded := seenLines.LoadOrStore(data, struct{}{}) 47 | if !loaded { 48 | out <- data 49 | } 50 | 51 | } 52 | r.Close() 53 | } 54 | }() 55 | return out, nil 56 | } 57 | 58 | func (f *FileWatcher) Close() { 59 | f.watcher.Stop() 60 | } 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue report 3 | about: Create a report to help us to improve the project 4 | labels: 'Type: Bug' 5 | 6 | --- 7 | 8 | 13 | 14 | 15 | 16 | ### Interactsh version: 17 | 18 | 19 | 20 | 21 | ### Current Behavior: 22 | 23 | 24 | ### Expected Behavior: 25 | 26 | 27 | ### Steps To Reproduce: 28 | 33 | 34 | 35 | ### Anything else: 36 | 37 | -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | ### Prerequisites 2 | - [ ] Install [Ansible](https://docs.ansible.com/ansible/latest/installation_guide/ 3 | installation_distros.html) on your local machine 4 | - eg: On ubuntu `sudo apt install ansible-core` 5 | - eg: On mac `brew install ansible` 6 | - [ ] Install resolvelib `sudo -H pip install -Iv 'resolvelib<0.6.0'` 7 | - [ ] Install ansible docker module `ansible-galaxy collection install community.docker` 8 | 9 | 10 | 11 | ## Using deploy.yaml 12 | - This Playbook is responsible for deploying the application to a remote server. 13 | - It will do the following things 14 | - Install required system packages 15 | - Install docker 16 | - Start the interactsh container 17 | 18 | ### Deploy 19 | - Open deploy.yaml and change the parameters in the `vars` section to match your environment/requirments. 20 | - Run `ansible-playbook deploy.yaml` to deploy the application. 21 | - You can also run `ansible-playbook deploy.yaml --extra-vars "container_tag=v1.1.2"` to pass the variables from the command line. 22 | eg: 23 | ```bash 24 | ansible-playbook deploy.yaml --extra-vars "container_tag=v1.1.2" 25 | ``` 26 | 27 | ### Add Grafana agent 28 | - To add grafana agent to collect node metrics and logs on you project 29 | Open grafan_agent.yaml update the variables as per your project and run following command 30 | ``` 31 | export GRAFANA_CLOUD=**** 32 | export PROM_URL=**** 33 | export PROM_PASS==**** 34 | export PROM_USERNAME==**** 35 | ansible-playbook grafana_agent.yaml 36 | ``` 37 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | # Maintain dependencies for GitHub Actions 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | target-branch: "dev" 15 | commit-message: 16 | prefix: "chore" 17 | include: "scope" 18 | labels: 19 | - "Type: Maintenance" 20 | 21 | # Maintain dependencies for go modules 22 | - package-ecosystem: "gomod" 23 | directory: "/" 24 | schedule: 25 | interval: "daily" 26 | target-branch: "dev" 27 | commit-message: 28 | prefix: "chore" 29 | include: "scope" 30 | labels: 31 | - "Type: Maintenance" 32 | 33 | # Maintain dependencies for docker 34 | - package-ecosystem: "docker" 35 | directory: ".github/docker/client/" 36 | schedule: 37 | interval: "weekly" 38 | target-branch: "dev" 39 | commit-message: 40 | prefix: "chore" 41 | include: "scope" 42 | labels: 43 | - "Type: Maintenance" 44 | 45 | - package-ecosystem: "docker" 46 | directory: ".github/docker/server/" 47 | schedule: 48 | interval: "weekly" 49 | target-branch: "dev" 50 | commit-message: 51 | prefix: "chore" 52 | include: "scope" 53 | labels: 54 | - "Type: Maintenance" -------------------------------------------------------------------------------- /pkg/server/http_server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestWriteResponseFromDynamicRequest(t *testing.T) { 14 | t.Run("status", func(t *testing.T) { 15 | req := httptest.NewRequest("GET", "http://example.com/?status=404", nil) 16 | w := httptest.NewRecorder() 17 | writeResponseFromDynamicRequest(w, req) 18 | 19 | resp := w.Result() 20 | require.Equal(t, http.StatusNotFound, resp.StatusCode, "could not get correct result") 21 | }) 22 | t.Run("delay", func(t *testing.T) { 23 | req := httptest.NewRequest("GET", "http://example.com/?delay=1", nil) 24 | w := httptest.NewRecorder() 25 | now := time.Now() 26 | writeResponseFromDynamicRequest(w, req) 27 | took := time.Since(now) 28 | 29 | require.Greater(t, took, 1*time.Second, "could not get correct delay") 30 | }) 31 | t.Run("body", func(t *testing.T) { 32 | req := httptest.NewRequest("GET", "http://example.com/?body=this+is+example+body", nil) 33 | w := httptest.NewRecorder() 34 | writeResponseFromDynamicRequest(w, req) 35 | 36 | resp := w.Result() 37 | body, _ := io.ReadAll(resp.Body) 38 | require.Equal(t, "this is example body", string(body), "could not get correct result") 39 | }) 40 | t.Run("header", func(t *testing.T) { 41 | req := httptest.NewRequest("GET", "http://example.com/?header=Key:value&header=Test:Another", nil) 42 | w := httptest.NewRecorder() 43 | writeResponseFromDynamicRequest(w, req) 44 | 45 | resp := w.Result() 46 | require.Equal(t, resp.Header.Get("Key"), "value", "could not get correct result") 47 | require.Equal(t, resp.Header.Get("Test"), "Another", "could not get correct result") 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/storage/util.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/rand" 8 | "crypto/rsa" 9 | "crypto/x509" 10 | "encoding/base64" 11 | "encoding/pem" 12 | "errors" 13 | "io" 14 | ) 15 | 16 | // ParseB64RSAPublicKeyFromPEM parses a base64 encoded rsa pem to a public key structure 17 | func ParseB64RSAPublicKeyFromPEM(pubPEM string) (*rsa.PublicKey, error) { 18 | decoded, err := base64.StdEncoding.DecodeString(pubPEM) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | block, _ := pem.Decode(decoded) 24 | if block == nil { 25 | return nil, errors.New("failed to parse PEM block containing the key") 26 | } 27 | 28 | pub, err := x509.ParsePKIXPublicKey(block.Bytes) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | switch pub := pub.(type) { 34 | case *rsa.PublicKey: 35 | return pub, nil 36 | default: 37 | break // fall through 38 | } 39 | return nil, errors.New("Key type is not RSA") 40 | } 41 | 42 | // AESEncrypt encrypts a message using AES and puts IV at the beginning of ciphertext. 43 | func AESEncrypt(key []byte, message []byte) (string, error) { 44 | block, err := aes.NewCipher(key) 45 | if err != nil { 46 | return "", err 47 | } 48 | // It's common to put IV at the beginning of the ciphertext. 49 | cipherText := make([]byte, aes.BlockSize+len(message)) 50 | iv := cipherText[:aes.BlockSize] 51 | if _, err = io.ReadFull(rand.Reader, iv); err != nil { 52 | return "", err 53 | } 54 | stream := cipher.NewCFBEncrypter(block, iv) 55 | stream.XORKeyStream(cipherText[aes.BlockSize:], message) 56 | encMessage := make([]byte, base64.StdEncoding.EncodedLen(len(cipherText))) 57 | base64.StdEncoding.Encode(encMessage, cipherText) 58 | return string(encMessage), nil 59 | } 60 | 61 | func AppendMany(sep string, slices ...[]byte) []byte { 62 | var final [][]byte 63 | for _, slice := range slices { 64 | if len(slice) == 0 { 65 | continue 66 | } 67 | final = append(final, slice) 68 | } 69 | return bytes.Join(final, []byte(sep)) 70 | } 71 | -------------------------------------------------------------------------------- /cmd/benchmark-server/load-testing/bench.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/miekg/dns" 9 | "github.com/projectdiscovery/interactsh/pkg/client" 10 | "github.com/projectdiscovery/retryabledns" 11 | "github.com/remeh/sizedwaitgroup" 12 | ) 13 | 14 | var ( 15 | serverURL = flag.String("url", "https://hackwithautomation.com", "URL of the interactsh server") 16 | serverIP = flag.String("ip", "138.68.140.25", "IP of benchmarked server") 17 | pollCount = flag.Int("poll-count", 10, "Number of poll interactions per registered URL") 18 | n = flag.Int("n", 1000, "Number of interactsh sessions to register") 19 | concurrency = flag.Int("c", 300, "Number of concurrent requests to send") 20 | token = flag.String("token", "gg", "Authentication token for the server") 21 | ) 22 | 23 | var ( 24 | errors = int64(0) 25 | requests = int64(0) 26 | polls = int64(0) 27 | ) 28 | 29 | func main() { 30 | flag.Parse() 31 | 32 | if err := process(); err != nil { 33 | log.Fatalf("Could not process: %s\n", err) 34 | } 35 | } 36 | 37 | func process() error { 38 | swg := sizedwaitgroup.New(*concurrency) 39 | for i := 0; i < *n; i++ { 40 | swg.Add() 41 | 42 | go func() { 43 | benchmarkServer() 44 | swg.Done() 45 | }() 46 | } 47 | swg.Wait() 48 | 49 | fmt.Printf("Send: errors=%v requests=%v polls=%v\n", errors, requests, polls) 50 | return nil 51 | } 52 | 53 | func benchmarkServer() { 54 | client, err := client.New(&client.Options{ 55 | ServerURL: *serverURL, 56 | Token: *token, 57 | }) 58 | if err != nil { 59 | errors++ 60 | log.Printf("Unexpected register response: %v\n", err) 61 | return 62 | } 63 | 64 | dnsClient, err := retryabledns.New([]string{*serverIP + ":53"}, 1) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | for i := 0; i < *pollCount; i++ { 69 | client.URL() 70 | 71 | polls++ 72 | data, err := dnsClient.Query(client.URL(), dns.TypeA) 73 | if err != nil { 74 | errors++ 75 | log.Printf("Unexpected resolve response: %v\n", err) 76 | continue 77 | } 78 | _ = data 79 | } 80 | requests++ 81 | } 82 | -------------------------------------------------------------------------------- /deploy/grafana_agent.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | become: true 3 | vars: 4 | env_name: "oast" 5 | container_name: "interactsh" 6 | grafana_agent_image: "grafana/agent:latest" 7 | grafana_cloud_url: "{{ lookup('env', 'GRAFANA_CLOUD') }}" 8 | prometheus_cloud_url: "{{ lookup('env', 'PROM_URL') }}" 9 | prometheus_cloud_password: "{{ lookup('env', 'PROM_PASS') }}" 10 | prometheus_cloud_username: "{{ lookup('env', 'PROM_USERNAME') }}" 11 | tasks: 12 | - name: create grafana directory for config 13 | file: 14 | path: "{{ item }}" 15 | state: directory 16 | loop: 17 | - /etc/grafana/ 18 | tags: dir 19 | 20 | - name: copy grafana agent config file 21 | template: 22 | src: agent.yaml.j2 23 | dest: "/etc/grafana/agent.yaml" 24 | tags: agent 25 | 26 | - name: pull Docker image of grafana agent 27 | community.docker.docker_image: 28 | name: "{{ grafana_agent_image }}" 29 | source: pull 30 | tags: agent 31 | 32 | - name: create Grafana agent container 33 | community.docker.docker_container: 34 | name: grafana_agent 35 | image: "{{ grafana_agent_image }}" 36 | restart_policy: always 37 | restart: true 38 | state: started 39 | volumes: 40 | - "/etc/grafana/agent.yaml:/etc/agent-config/agent.yaml:ro" 41 | - "/proc:/host/proc:ro" 42 | - "/tmp/agent:/etc/agent" 43 | - "/var/lib/docker/:/var/lib/docker:ro" 44 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 45 | - "/run/containerd/containerd.sock:/run/containerd/containerd.sock:ro" 46 | - "/var/lib/docker/containers:/var/lib/docker/containers:ro" 47 | - "/:/rootfs:ro" 48 | - "/var/run:/var/run:ro" 49 | - "/sys:/sys:ro" 50 | devices: 51 | - "/dev/kmsg:/dev/kmsg" 52 | privileged: true 53 | entrypoint: 54 | - "/bin/agent" 55 | - "-config.file=/etc/agent-config/agent.yaml" 56 | - "-metrics.wal-directory=/tmp/agent/wal" 57 | - "-server.register-instrumentation=false" 58 | network_mode: "host" 59 | pid_mode: "host" 60 | -------------------------------------------------------------------------------- /internal/runner/healthcheck.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/projectdiscovery/interactsh/pkg/options" 10 | fileutil "github.com/projectdiscovery/utils/file" 11 | ) 12 | 13 | // DoHealthCheck performs healthcheck on server and client 14 | func DoHealthCheck(cfgFilePath string) string { 15 | // RW permissions on config file 16 | var test strings.Builder 17 | test.WriteString(fmt.Sprintf("Version: %s\n", options.Version)) 18 | test.WriteString(fmt.Sprintf("Operative System: %s\n", runtime.GOOS)) 19 | test.WriteString(fmt.Sprintf("Architecture: %s\n", runtime.GOARCH)) 20 | test.WriteString(fmt.Sprintf("Go Version: %s\n", runtime.Version())) 21 | test.WriteString(fmt.Sprintf("Compiler: %s\n", runtime.Compiler)) 22 | 23 | var testResult string 24 | ok, err := fileutil.IsReadable(cfgFilePath) 25 | if ok { 26 | testResult = "Ok" 27 | } else { 28 | testResult = "Ko" 29 | } 30 | if err != nil { 31 | testResult += fmt.Sprintf(" (%s)", err) 32 | } 33 | test.WriteString(fmt.Sprintf("Config file \"%s\" Read => %s\n", cfgFilePath, testResult)) 34 | ok, err = fileutil.IsWriteable(cfgFilePath) 35 | if ok { 36 | testResult = "Ok" 37 | } else { 38 | testResult = "Ko" 39 | } 40 | if err != nil { 41 | testResult += fmt.Sprintf(" (%s)", err) 42 | } 43 | test.WriteString(fmt.Sprintf("Config file \"%s\" Write => %s\n", cfgFilePath, testResult)) 44 | c3, err := net.Dial("udp", "scanme.sh:53") 45 | if err == nil && c3 != nil { 46 | c3.Close() 47 | } 48 | testResult = "Ok" 49 | if err != nil { 50 | testResult = fmt.Sprintf("Ko (%s)", err) 51 | } 52 | test.WriteString(fmt.Sprintf("UDP connectivity to scanme.sh:53 => %s\n", testResult)) 53 | c4, err := net.Dial("tcp4", "scanme.sh:80") 54 | if err == nil && c4 != nil { 55 | c4.Close() 56 | } 57 | testResult = "Ok" 58 | if err != nil { 59 | testResult = fmt.Sprintf("Ko (%s)", err) 60 | } 61 | test.WriteString(fmt.Sprintf("IPv4 connectivity to scanme.sh:80 => %s\n", testResult)) 62 | c6, err := net.Dial("tcp6", "scanme.sh:80") 63 | if err == nil && c6 != nil { 64 | c6.Close() 65 | } 66 | testResult = "Ok" 67 | if err != nil { 68 | testResult = fmt.Sprintf("Ko (%s)", err) 69 | } 70 | test.WriteString(fmt.Sprintf("IPv6 connectivity to scanme.sh:80 => %s\n", testResult)) 71 | 72 | return test.String() 73 | } 74 | -------------------------------------------------------------------------------- /deploy/deploy.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | become: true 3 | vars: 4 | container_name: "interactsh" 5 | container_tag: "v1.1.5" 6 | container_image: "projectdiscovery/interactsh-server:{{container_tag}}" 7 | container_command: "-dr -d {{domain_name}} -metrics" 8 | certmagic_host_path: "/root/.local/share/certmagic" 9 | tasks: 10 | - name: Install aptitude 11 | apt: 12 | name: aptitude 13 | state: latest 14 | update_cache: true 15 | tags: 16 | - apt 17 | - setup 18 | 19 | - name: Install required system packages 20 | apt: 21 | pkg: 22 | - apt-transport-https 23 | - ca-certificates 24 | - curl 25 | - software-properties-common 26 | - python3-pip 27 | - virtualenv 28 | - python3-setuptools 29 | - lsb-release 30 | - gnupg 31 | state: latest 32 | update_cache: true 33 | tags: 34 | - apt 35 | - setup 36 | 37 | 38 | - name: Add Docker GPG apt Key 39 | apt_key: 40 | url: https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg 41 | state: present 42 | tags: 43 | - docker 44 | - setup 45 | 46 | - name: Add Docker Repository 47 | apt_repository: 48 | repo: deb https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable 49 | state: present 50 | tags: 51 | - docker 52 | - setup 53 | 54 | - name: Update apt and install docker-ce 55 | apt: 56 | name: docker-ce 57 | state: latest 58 | update_cache: true 59 | tags: 60 | - docker 61 | - setup 62 | 63 | - name: Install Docker Module for Python 64 | pip: 65 | name: docker 66 | tags: 67 | - docker 68 | - setup 69 | 70 | - name: Make sure certmagic directory is created 71 | file: 72 | path: "{{ item }}" 73 | state: directory 74 | loop: 75 | - "{{ certmagic_host_path }}" 76 | tags: dir 77 | 78 | - name: Pull Docker image 79 | community.docker.docker_image: 80 | name: "{{ container_image }}" 81 | source: pull 82 | tags: 83 | - deploy 84 | - pull 85 | 86 | - name: Launch interactsh docker container 87 | community.docker.docker_container: 88 | name: "{{ container_name }}" 89 | image: "{{ container_image }}" 90 | command: "{{ container_command }}" 91 | memory: "4g" 92 | memory_swap: "-1" 93 | network_mode: host 94 | restart: true # always restart the container 95 | restart_policy: "unless-stopped" 96 | volumes: 97 | - "{{certmagic_host_path}}:{{certmagic_host_path}}" 98 | state: started 99 | tags: 100 | - deploy 101 | - test 102 | -------------------------------------------------------------------------------- /pkg/server/acme/records_store.go: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/chinzhiweiblank/coredns-acme/blob/e0cdfbdd78adfcc6c2d098255902f64ec60daecb/provider.go 2 | // Copyright @chinzhiweiblank under Apache License 2.0 3 | package acme 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/libdns/libdns" 12 | ) 13 | 14 | type RecordStore struct { 15 | entries []libdns.Record 16 | } 17 | 18 | type Provider struct { 19 | sync.Mutex 20 | recordMap map[string]*RecordStore 21 | } 22 | 23 | func NewProvider() *Provider { 24 | return &Provider{Mutex: sync.Mutex{}, recordMap: make(map[string]*RecordStore)} 25 | } 26 | 27 | func (p *Provider) getZoneRecords(_ context.Context, zoneName string) *RecordStore { 28 | return p.recordMap[zoneName] 29 | } 30 | 31 | func compareRecords(a, b libdns.Record) bool { 32 | return a.Type == b.Type && a.Name == b.Name && a.Value == b.Value && a.TTL == b.TTL 33 | } 34 | 35 | func (r *RecordStore) deleteRecords(recs []libdns.Record) []libdns.Record { 36 | var deletedRecords []libdns.Record 37 | for i, entry := range r.entries { 38 | for _, record := range recs { 39 | if compareRecords(entry, record) { 40 | deletedRecords = append(deletedRecords, record) 41 | r.entries = append(r.entries[:i], r.entries[i+1:]...) 42 | } 43 | } 44 | } 45 | return deletedRecords 46 | } 47 | 48 | func (p *Provider) AppendRecords(ctx context.Context, zoneName string, recs []libdns.Record) ([]libdns.Record, error) { 49 | p.Lock() 50 | defer p.Unlock() 51 | zoneRecordStore := p.getZoneRecords(ctx, zoneName) 52 | if zoneRecordStore == nil { 53 | zoneRecordStore = new(RecordStore) 54 | p.recordMap[zoneName] = zoneRecordStore 55 | } 56 | 57 | // ACME DNS challenge need only one record, old record should be deleted 58 | if strings.HasPrefix(strings.ToLower(zoneName), DNSChallengeString) { 59 | zoneRecordStore.entries = recs 60 | } else { 61 | zoneRecordStore.entries = append(zoneRecordStore.entries, recs...) 62 | } 63 | 64 | return zoneRecordStore.entries, nil 65 | } 66 | 67 | func (p *Provider) DeleteRecords(ctx context.Context, zoneName string, recs []libdns.Record) ([]libdns.Record, error) { 68 | p.Lock() 69 | defer p.Unlock() 70 | zoneRecordStore := p.getZoneRecords(ctx, zoneName) 71 | if zoneRecordStore == nil { 72 | return nil, nil 73 | } 74 | deletedRecords := zoneRecordStore.deleteRecords(recs) 75 | return deletedRecords, nil 76 | } 77 | 78 | func (p *Provider) GetRecords(ctx context.Context, zoneName string) ([]libdns.Record, error) { 79 | p.Lock() 80 | defer p.Unlock() 81 | records := p.getZoneRecords(ctx, zoneName) 82 | if records == nil { 83 | return nil, fmt.Errorf("no records were found for %v", zoneName) 84 | } 85 | return records.entries, nil 86 | } 87 | 88 | var ( 89 | _ libdns.RecordGetter = (*Provider)(nil) 90 | _ libdns.RecordAppender = (*Provider)(nil) 91 | _ libdns.RecordDeleter = (*Provider)(nil) 92 | ) 93 | 94 | const ( 95 | DNSChallengeString = "_acme-challenge." 96 | CertificateAuthority = "letsencrypt.org." 97 | ) 98 | -------------------------------------------------------------------------------- /cmd/benchmark-server/duration-testing/bench.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "io" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "sync" 12 | "time" 13 | 14 | "github.com/miekg/dns" 15 | "github.com/projectdiscovery/interactsh/pkg/client" 16 | "github.com/projectdiscovery/interactsh/pkg/server" 17 | "github.com/projectdiscovery/retryabledns" 18 | "go.uber.org/ratelimit" 19 | ) 20 | 21 | var ( 22 | serverURL = flag.String("url", "http://192.168.1.86", "URL of the interactsh server") 23 | serverIP = flag.String("ip", "192.168.1.86", "IP of benchmarked server") 24 | n = flag.Int("n", 100, "Number of interactsh clients to register") 25 | pollintInterval = flag.Int("d", 30, "Polling interval in seconds") 26 | interactionsRateLimit = flag.Int("rl", 10, "Max interactions per second per session") 27 | ) 28 | 29 | var ( 30 | clients []*client.Client 31 | ctx context.Context 32 | ctxCancel context.CancelFunc 33 | ) 34 | 35 | func main() { 36 | flag.Parse() 37 | 38 | ctx, ctxCancel = context.WithCancel(context.Background()) 39 | clients = make([]*client.Client, *n) 40 | 41 | if err := process(); err != nil { 42 | log.Fatalf("Could not process: %s\n", err) 43 | } 44 | 45 | c := make(chan os.Signal, 1) 46 | signal.Notify(c, os.Interrupt) 47 | for range c { 48 | ctxCancel() 49 | for _, client := range clients { 50 | _ = client.StopPolling() 51 | _ = client.Close() 52 | } 53 | } 54 | } 55 | 56 | func process() error { 57 | var swg sync.WaitGroup 58 | for i := 0; i < *n; i++ { 59 | swg.Add(1) 60 | 61 | go func(idx int) { 62 | defer swg.Done() 63 | 64 | startClient(idx) 65 | }(i) 66 | } 67 | swg.Wait() 68 | 69 | return nil 70 | } 71 | 72 | func startClient(idx int) { 73 | client, err := client.New(&client.Options{ 74 | ServerURL: *serverURL, 75 | }) 76 | if err != nil { 77 | log.Printf("Unexpected register response: %v\n", err) 78 | return 79 | } 80 | 81 | clients[idx] = client 82 | 83 | log.Printf("client %d registered, sample url: %s\n", idx, client.URL()) 84 | _ = client.StartPolling(time.Duration(*pollintInterval)*time.Second, func(interaction *server.Interaction) { 85 | log.Printf("Client %d polled interaction: %s\n", idx, interaction.FullId) 86 | }) 87 | 88 | dnsClient, err := retryabledns.New([]string{*serverIP + ":53"}, 1) 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | 93 | // simulate continous interactions 94 | rateLimiter := ratelimit.New(*interactionsRateLimit) 95 | for { 96 | select { 97 | case <-ctx.Done(): 98 | return 99 | default: 100 | rateLimiter.Take() 101 | req, err := http.NewRequest(http.MethodGet, *serverURL, nil) 102 | if err != nil { 103 | log.Printf("%s\n", err) 104 | continue 105 | } 106 | req.Host = client.URL() 107 | resp, err := http.DefaultClient.Do(req) 108 | if err != nil { 109 | log.Printf("client %d failed to send http request\n", idx) 110 | } else if resp != nil { 111 | _, _ = io.Copy(io.Discard, resp.Body) 112 | resp.Body.Close() 113 | log.Printf("Client %d sent HTTP request: %d\n", idx, resp.StatusCode) 114 | } 115 | 116 | data, err := dnsClient.Query(client.URL(), dns.TypeA) 117 | if err != nil { 118 | log.Printf("client %d failed to send dns request\n", idx) 119 | } 120 | log.Printf("Client %d sent DNS request: %s\n", idx, data.StatusCode) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /pkg/server/smb_server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | "sync/atomic" 11 | "time" 12 | 13 | jsoniter "github.com/json-iterator/go" 14 | "github.com/projectdiscovery/gologger" 15 | "github.com/projectdiscovery/interactsh/pkg/filewatcher" 16 | fileutil "github.com/projectdiscovery/utils/file" 17 | stringsutil "github.com/projectdiscovery/utils/strings" 18 | ) 19 | 20 | var smbMonitorList map[string]string = map[string]string{ 21 | // search term : extract after 22 | "INFO: ": "INFO: ", 23 | } 24 | 25 | // SMBServer is a smb wrapper server instance 26 | type SMBServer struct { 27 | options *Options 28 | LogFile string 29 | ipAddress net.IP 30 | cmd *exec.Cmd 31 | tmpFile string 32 | } 33 | 34 | // NewSMBServer returns a new SMB server. 35 | func NewSMBServer(options *Options) (*SMBServer, error) { 36 | server := &SMBServer{ 37 | options: options, 38 | ipAddress: net.ParseIP(options.IPAddress), 39 | } 40 | return server, nil 41 | } 42 | 43 | // ListenAndServe listens on smb port 44 | func (h *SMBServer) ListenAndServe(smbAlive chan bool) error { 45 | smbAlive <- true 46 | defer func() { 47 | smbAlive <- false 48 | }() 49 | tmpFile, err := os.CreateTemp("", "") 50 | if err != nil { 51 | return err 52 | } 53 | h.tmpFile = tmpFile.Name() 54 | tmpFile.Close() 55 | // execute smb_server.py - only works with ./interactsh-server 56 | cmdLine := fmt.Sprintf("python3 smb_server.py %s %d", h.tmpFile, h.options.SmbPort) 57 | args := strings.Fields(cmdLine) 58 | h.cmd = exec.Command(args[0], args[1:]...) 59 | err = h.cmd.Start() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | // watch output file 65 | outputFile := h.tmpFile 66 | // wait until the file is created 67 | for !fileutil.FileExists(outputFile) { 68 | time.Sleep(1 * time.Second) 69 | } 70 | fw, err := filewatcher.New(filewatcher.Options{ 71 | Interval: time.Duration(5 * time.Second), 72 | File: outputFile, 73 | }) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | ch, err := fw.Watch() 79 | if err != nil { 80 | return err 81 | } 82 | 83 | // This fetches the content at each change. 84 | go func() { 85 | for data := range ch { 86 | atomic.AddUint64(&h.options.Stats.Smb, 1) 87 | for searchTerm, extractAfter := range smbMonitorList { 88 | if strings.Contains(data, searchTerm) { 89 | smbData, err := stringsutil.After(data, extractAfter) 90 | if err != nil { 91 | gologger.Warning().Msgf("Could not get smb interaction: %s\n", err) 92 | continue 93 | } 94 | 95 | // Correlation id doesn't apply here, we skip encryption 96 | interaction := &Interaction{ 97 | Protocol: "smb", 98 | RawRequest: smbData, 99 | Timestamp: time.Now(), 100 | } 101 | buffer := &bytes.Buffer{} 102 | if err := jsoniter.NewEncoder(buffer).Encode(interaction); err != nil { 103 | gologger.Warning().Msgf("Could not encode smb interaction: %s\n", err) 104 | } else { 105 | gologger.Debug().Msgf("SMB Interaction: \n%s\n", buffer.String()) 106 | if err := h.options.Storage.AddInteractionWithId(h.options.Token, buffer.Bytes()); err != nil { 107 | gologger.Warning().Msgf("Could not store dns interaction: %s\n", err) 108 | } 109 | } 110 | } 111 | } 112 | } 113 | }() 114 | 115 | return h.cmd.Wait() 116 | } 117 | 118 | func (h *SMBServer) Close() { 119 | _ = h.cmd.Process.Kill() 120 | if fileutil.FileExists(h.tmpFile) { 121 | os.RemoveAll(h.tmpFile) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /pkg/server/responder_server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | jsoniter "github.com/json-iterator/go" 13 | "github.com/projectdiscovery/gologger" 14 | "github.com/projectdiscovery/interactsh/pkg/filewatcher" 15 | fileutil "github.com/projectdiscovery/utils/file" 16 | stringsutil "github.com/projectdiscovery/utils/strings" 17 | ) 18 | 19 | var responderMonitorList map[string]string = map[string]string{ 20 | // search term : extract after 21 | "NTLMv2-SSP Hash": "NTLMv2-SSP Hash : ", 22 | } 23 | 24 | // ResponderServer is a Responder wrapper server instance 25 | type ResponderServer struct { 26 | options *Options 27 | LogFile string 28 | ipAddress net.IP 29 | cmd *exec.Cmd 30 | tmpFolder string 31 | } 32 | 33 | // NewResponderServer returns a new SMB server. 34 | func NewResponderServer(options *Options) (*ResponderServer, error) { 35 | server := &ResponderServer{ 36 | options: options, 37 | ipAddress: net.ParseIP(options.IPAddress), 38 | } 39 | return server, nil 40 | } 41 | 42 | // ListenAndServe listens on various responder ports 43 | func (h *ResponderServer) ListenAndServe(responderAlive chan bool) error { 44 | responderAlive <- true 45 | defer func() { 46 | responderAlive <- false 47 | }() 48 | tmpFolder, err := os.MkdirTemp("", "") 49 | if err != nil { 50 | return err 51 | } 52 | h.tmpFolder = tmpFolder 53 | // execute dockerized responder 54 | cmdLine := "docker run -p 137:137/udp -p 138:138/udp -p 389:389 -p 1433:1433 -p 1434:1434/udp -p 135:135 -p 139:139 -p 445:445 -p 21:21 -p 3141:3141 -p 110:110 -p 3128:3128 -p 5355:5355/udp -v " + h.tmpFolder + ":/opt/Responder/logs --rm interactsh:latest" 55 | args := strings.Fields(cmdLine) 56 | h.cmd = exec.Command(args[0], args[1:]...) 57 | err = h.cmd.Start() 58 | if err != nil { 59 | return err 60 | } 61 | 62 | // watch output file 63 | outputFile := filepath.Join(h.tmpFolder, "Responder-Session.log") 64 | // wait until the file is created 65 | for !fileutil.FileExists(outputFile) { 66 | time.Sleep(1 * time.Second) 67 | } 68 | fw, err := filewatcher.New(filewatcher.Options{ 69 | Interval: time.Duration(5 * time.Second), 70 | File: outputFile, 71 | }) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | ch, err := fw.Watch() 77 | if err != nil { 78 | return err 79 | } 80 | 81 | // This fetches the content at each change. 82 | go func() { 83 | for data := range ch { 84 | for searchTerm, extractAfter := range responderMonitorList { 85 | if strings.Contains(data, searchTerm) { 86 | responderData, err := stringsutil.After(data, extractAfter) 87 | if err != nil { 88 | gologger.Warning().Msgf("Could not get responder interaction: %s\n", err) 89 | continue 90 | } 91 | 92 | // Correlation id doesn't apply here, we skip encryption 93 | interaction := &Interaction{ 94 | Protocol: "responder", 95 | RawRequest: responderData, 96 | Timestamp: time.Now(), 97 | } 98 | buffer := &bytes.Buffer{} 99 | if err := jsoniter.NewEncoder(buffer).Encode(interaction); err != nil { 100 | gologger.Warning().Msgf("Could not encode responder interaction: %s\n", err) 101 | } else { 102 | gologger.Debug().Msgf("Responder Interaction: \n%s\n", buffer.String()) 103 | if err := h.options.Storage.AddInteractionWithId(h.options.Token, buffer.Bytes()); err != nil { 104 | gologger.Warning().Msgf("Could not store dns interaction: %s\n", err) 105 | } 106 | } 107 | } 108 | } 109 | } 110 | }() 111 | 112 | return h.cmd.Wait() 113 | } 114 | 115 | func (h *ResponderServer) Close() { 116 | _ = h.cmd.Process.Kill() 117 | if fileutil.FolderExists(h.tmpFolder) { 118 | os.RemoveAll(h.tmpFolder) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /pkg/server/metrics.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "runtime" 5 | 6 | units "github.com/docker/go-units" 7 | "github.com/mackerelio/go-osstat/network" 8 | "github.com/projectdiscovery/interactsh/pkg/storage" 9 | ) 10 | 11 | type Metrics struct { 12 | Dns uint64 `json:"dns"` 13 | Ftp uint64 `json:"ftp"` 14 | Http uint64 `json:"http"` 15 | Ldap uint64 `json:"ldap"` 16 | Smb uint64 `json:"smb"` 17 | Smtp uint64 `json:"smtp"` 18 | Sessions int64 `json:"sessions"` 19 | Cache *storage.CacheMetrics `json:"cache"` 20 | Memory *MemoryMetrics `json:"memory"` 21 | Cpu *CpuStats `json:"cpu"` 22 | Network *NetworkStats `json:"network"` 23 | } 24 | 25 | func GetCacheMetrics(options *Options) *storage.CacheMetrics { 26 | cacheMetrics, _ := options.Storage.GetCacheMetrics() 27 | return cacheMetrics 28 | } 29 | 30 | type MemoryMetrics struct { 31 | Alloc string `json:"alloc"` 32 | TotalAlloc string `json:"total_alloc"` 33 | Sys string `json:"sys"` 34 | Lookups uint64 `json:"lookups"` 35 | Mallocs uint64 `json:"mallocs"` 36 | Frees uint64 `json:"frees"` 37 | HeapAlloc string `json:"heap_allo"` 38 | HeapSys string `json:"heap_sys"` 39 | HeapIdle string `json:"head_idle"` 40 | HeapInuse string `json:"heap_in_use"` 41 | HeapReleased string `json:"heap_released"` 42 | HeapObjects uint64 `json:"heap_objects"` 43 | StackInuse string `json:"stack_in_use"` 44 | StackSys string `json:"stack_sys"` 45 | MSpanInuse string `json:"mspan_in_use"` 46 | MSpanSys string `json:"mspan_sys"` 47 | MCacheInuse string `json:"mcache_in_use"` 48 | MCacheSys string `json:"mcache_sys"` 49 | } 50 | 51 | func GetMemoryMetrics() *MemoryMetrics { 52 | var mStats runtime.MemStats 53 | runtime.ReadMemStats(&mStats) 54 | return &MemoryMetrics{ 55 | Alloc: units.HumanSize(float64(mStats.Alloc)), 56 | TotalAlloc: units.HumanSize(float64(mStats.TotalAlloc)), 57 | Sys: units.HumanSize(float64(mStats.Sys)), 58 | Lookups: mStats.Lookups, 59 | Mallocs: mStats.Mallocs, 60 | Frees: mStats.Frees, 61 | HeapAlloc: units.HumanSize(float64(mStats.HeapAlloc)), 62 | HeapSys: units.HumanSize(float64(mStats.HeapSys)), 63 | HeapIdle: units.HumanSize(float64(mStats.HeapIdle)), 64 | HeapInuse: units.HumanSize(float64(mStats.HeapInuse)), 65 | HeapReleased: units.HumanSize(float64(mStats.HeapReleased)), 66 | HeapObjects: mStats.HeapObjects, 67 | StackInuse: units.HumanSize(float64(mStats.StackInuse)), 68 | StackSys: units.HumanSize(float64(mStats.StackSys)), 69 | MSpanInuse: units.HumanSize(float64(mStats.MSpanInuse)), 70 | MSpanSys: units.HumanSize(float64(mStats.MSpanSys)), 71 | MCacheInuse: units.HumanSize(float64(mStats.MCacheInuse)), 72 | MCacheSys: units.HumanSize(float64(mStats.MCacheSys)), 73 | } 74 | } 75 | 76 | type CpuStats struct { 77 | User uint64 `json:"user"` 78 | System uint64 `json:"system"` 79 | Idle uint64 `json:"idle"` 80 | Nice uint64 `json:"nice"` 81 | Total uint64 `json:"total"` 82 | } 83 | 84 | func GetCpuMetrics() (cpuStats *CpuStats) { 85 | cpuStats, _ = getCPUStats() 86 | return cpuStats 87 | } 88 | 89 | type NetworkStats struct { 90 | Rx string `json:"received"` 91 | rxBytes uint64 92 | Tx string `json:"transmitted"` 93 | txBytes uint64 94 | } 95 | 96 | func GetNetworkMetrics() *NetworkStats { 97 | networkStats := &NetworkStats{} 98 | if nss, err := network.Get(); err == nil { 99 | for _, ns := range nss { 100 | networkStats.rxBytes += ns.RxBytes 101 | networkStats.txBytes += ns.TxBytes 102 | } 103 | } 104 | networkStats.Rx = units.HumanSize(float64(networkStats.rxBytes)) 105 | networkStats.Tx = units.HumanSize(float64(networkStats.txBytes)) 106 | return networkStats 107 | } 108 | -------------------------------------------------------------------------------- /pkg/options/server_options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "github.com/projectdiscovery/goflags" 5 | "github.com/projectdiscovery/interactsh/pkg/server" 6 | ) 7 | 8 | type CLIServerOptions struct { 9 | Config string 10 | Version bool 11 | Debug bool 12 | Domains goflags.StringSlice 13 | DnsPort int 14 | IPAddress string 15 | ListenIP string 16 | HttpPort int 17 | HttpsPort int 18 | Hostmasters []string 19 | LdapWithFullLogger bool 20 | Eviction int 21 | NoEviction bool 22 | Responder bool 23 | Smb bool 24 | SmbPort int 25 | SmtpPort int 26 | SmtpsPort int 27 | SmtpAutoTLSPort int 28 | FtpPort int 29 | LdapPort int 30 | Ftp bool 31 | Auth bool 32 | HTTPIndex string 33 | HTTPDirectory string 34 | Token string 35 | OriginURL string 36 | RootTLD bool 37 | FTPDirectory string 38 | SkipAcme bool 39 | DynamicResp bool 40 | CorrelationIdLength int 41 | CorrelationIdNonceLength int 42 | ScanEverywhere bool 43 | CertificatePath string 44 | CustomRecords string 45 | PrivateKeyPath string 46 | OriginIPHeader string 47 | DiskStorage bool 48 | DiskStoragePath string 49 | EnablePprof bool 50 | EnableMetrics bool 51 | Verbose bool 52 | DisableUpdateCheck bool 53 | NoVersionHeader bool 54 | HeaderServer string 55 | } 56 | 57 | func (cliServerOptions *CLIServerOptions) AsServerOptions() *server.Options { 58 | return &server.Options{ 59 | Domains: cliServerOptions.Domains, 60 | DnsPort: cliServerOptions.DnsPort, 61 | IPAddress: cliServerOptions.IPAddress, 62 | ListenIP: cliServerOptions.ListenIP, 63 | HttpPort: cliServerOptions.HttpPort, 64 | HttpsPort: cliServerOptions.HttpsPort, 65 | Hostmasters: cliServerOptions.Hostmasters, 66 | SmbPort: cliServerOptions.SmbPort, 67 | SmtpPort: cliServerOptions.SmtpPort, 68 | SmtpsPort: cliServerOptions.SmtpsPort, 69 | SmtpAutoTLSPort: cliServerOptions.SmtpAutoTLSPort, 70 | FtpPort: cliServerOptions.FtpPort, 71 | LdapPort: cliServerOptions.LdapPort, 72 | Auth: cliServerOptions.Auth, 73 | HTTPIndex: cliServerOptions.HTTPIndex, 74 | HTTPDirectory: cliServerOptions.HTTPDirectory, 75 | Token: cliServerOptions.Token, 76 | Version: Version, 77 | DynamicResp: cliServerOptions.DynamicResp, 78 | OriginURL: cliServerOptions.OriginURL, 79 | RootTLD: cliServerOptions.RootTLD, 80 | FTPDirectory: cliServerOptions.FTPDirectory, 81 | CorrelationIdLength: cliServerOptions.CorrelationIdLength, 82 | CorrelationIdNonceLength: cliServerOptions.CorrelationIdNonceLength, 83 | ScanEverywhere: cliServerOptions.ScanEverywhere, 84 | CertificatePath: cliServerOptions.CertificatePath, 85 | CustomRecords: cliServerOptions.CustomRecords, 86 | PrivateKeyPath: cliServerOptions.PrivateKeyPath, 87 | OriginIPHeader: cliServerOptions.OriginIPHeader, 88 | DiskStorage: cliServerOptions.DiskStorage, 89 | DiskStoragePath: cliServerOptions.DiskStoragePath, 90 | EnableMetrics: cliServerOptions.EnableMetrics, 91 | NoVersionHeader: cliServerOptions.NoVersionHeader, 92 | HeaderServer: cliServerOptions.HeaderServer, 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/projectdiscovery/interactsh 2 | 3 | go 1.20 4 | 5 | require ( 6 | git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a 7 | github.com/Mzack9999/ldapserver v1.0.2-0.20211229000134-b44a0d6ad0dd 8 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 9 | github.com/caddyserver/certmagic v0.18.2 10 | github.com/docker/go-units v0.5.0 11 | github.com/goburrow/cache v0.1.4 12 | github.com/google/uuid v1.3.0 13 | github.com/json-iterator/go v1.1.12 14 | github.com/libdns/libdns v0.2.1 15 | github.com/mackerelio/go-osstat v0.2.4 16 | github.com/miekg/dns v1.1.55 17 | github.com/pkg/errors v0.9.1 18 | github.com/projectdiscovery/asnmap v1.0.4 19 | github.com/projectdiscovery/goflags v0.1.10 20 | github.com/projectdiscovery/gologger v1.1.10 21 | github.com/projectdiscovery/retryabledns v1.0.30 22 | github.com/projectdiscovery/retryablehttp-go v1.0.18 23 | github.com/projectdiscovery/utils v0.0.39 24 | github.com/remeh/sizedwaitgroup v1.0.0 25 | github.com/rs/xid v1.5.0 26 | github.com/stretchr/testify v1.8.4 27 | github.com/syndtr/goleveldb v1.0.0 28 | go.uber.org/multierr v1.11.0 29 | go.uber.org/ratelimit v0.2.0 30 | go.uber.org/zap v1.24.0 31 | goftp.io/server/v2 v2.0.1 32 | gopkg.in/corvus-ch/zbase32.v1 v1.0.0 33 | gopkg.in/yaml.v3 v3.0.1 34 | ) 35 | 36 | require ( 37 | aead.dev/minisign v0.2.0 // indirect 38 | github.com/Masterminds/semver/v3 v3.2.1 // indirect 39 | github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect 40 | github.com/VividCortex/ewma v1.2.0 // indirect 41 | github.com/alecthomas/chroma v0.10.0 // indirect 42 | github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect 43 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 44 | github.com/aymerick/douceur v0.2.0 // indirect 45 | github.com/charmbracelet/glamour v0.6.0 // indirect 46 | github.com/cheggaaa/pb/v3 v3.1.2 // indirect 47 | github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect 48 | github.com/davecgh/go-spew v1.1.1 // indirect 49 | github.com/dlclark/regexp2 v1.8.1 // indirect 50 | github.com/dsnet/compress v0.0.1 // indirect 51 | github.com/fatih/color v1.14.1 // indirect 52 | github.com/golang/protobuf v1.5.2 // indirect 53 | github.com/golang/snappy v0.0.4 // indirect 54 | github.com/google/go-github/v30 v30.1.0 // indirect 55 | github.com/google/go-querystring v1.1.0 // indirect 56 | github.com/gorilla/css v1.0.0 // indirect 57 | github.com/klauspost/cpuid/v2 v2.2.5 // indirect 58 | github.com/kr/pretty v0.3.1 // indirect 59 | github.com/logrusorgru/aurora v2.0.3+incompatible // indirect 60 | github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3 // indirect 61 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 62 | github.com/mattn/go-colorable v0.1.13 // indirect 63 | github.com/mattn/go-isatty v0.0.17 // indirect 64 | github.com/mattn/go-runewidth v0.0.14 // indirect 65 | github.com/mholt/acmez v1.2.0 // indirect 66 | github.com/mholt/archiver v3.1.1+incompatible // indirect 67 | github.com/microcosm-cc/bluemonday v1.0.24 // indirect 68 | github.com/minio/selfupdate v0.6.0 // indirect 69 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 70 | github.com/modern-go/reflect2 v1.0.2 // indirect 71 | github.com/muesli/reflow v0.3.0 // indirect 72 | github.com/muesli/termenv v0.15.1 // indirect 73 | github.com/nwaples/rardecode v1.1.0 // indirect 74 | github.com/olekukonko/tablewriter v0.0.5 // indirect 75 | github.com/pierrec/lz4 v2.6.0+incompatible // indirect 76 | github.com/pmezard/go-difflib v1.0.0 // indirect 77 | github.com/projectdiscovery/blackrock v0.0.1 // indirect 78 | github.com/projectdiscovery/mapcidr v1.1.1 // indirect 79 | github.com/rivo/uniseg v0.4.4 // indirect 80 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect 81 | github.com/ulikunitz/xz v0.5.8 // indirect 82 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 83 | github.com/yuin/goldmark v1.5.4 // indirect 84 | github.com/yuin/goldmark-emoji v1.0.1 // indirect 85 | go.uber.org/atomic v1.11.0 // indirect 86 | golang.org/x/crypto v0.10.0 // indirect 87 | golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect 88 | golang.org/x/mod v0.11.0 // indirect 89 | golang.org/x/net v0.11.0 // indirect 90 | golang.org/x/oauth2 v0.9.0 // indirect 91 | golang.org/x/sys v0.9.0 // indirect 92 | golang.org/x/text v0.10.0 // indirect 93 | golang.org/x/tools v0.10.0 // indirect 94 | google.golang.org/appengine v1.6.7 // indirect 95 | google.golang.org/protobuf v1.28.1 // indirect 96 | gopkg.in/djherbis/times.v1 v1.3.0 // indirect 97 | ) 98 | -------------------------------------------------------------------------------- /pkg/storage/storagedb_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/sha256" 9 | "crypto/x509" 10 | "encoding/base64" 11 | "encoding/pem" 12 | "strconv" 13 | "testing" 14 | "time" 15 | 16 | "github.com/goburrow/cache" 17 | "github.com/google/uuid" 18 | "github.com/rs/xid" 19 | "github.com/stretchr/testify/require" 20 | ) 21 | 22 | func TestStorageSetIDPublicKey(t *testing.T) { 23 | mem, err := New(&Options{EvictionTTL: 1 * time.Hour}) 24 | require.Nil(t, err) 25 | 26 | secret := uuid.New().String() 27 | correlationID := xid.New().String() 28 | 29 | // Generate a 2048-bit private-key 30 | priv, err := rsa.GenerateKey(rand.Reader, 2048) 31 | require.Nil(t, err, "could not generate rsa key") 32 | 33 | pub := priv.Public() 34 | 35 | pubkeyBytes, err := x509.MarshalPKIXPublicKey(pub) 36 | require.Nil(t, err, "could not marshal public key") 37 | 38 | pubkeyPem := pem.EncodeToMemory(&pem.Block{ 39 | Type: "RSA PUBLIC KEY", 40 | Bytes: pubkeyBytes, 41 | }) 42 | 43 | encoded := base64.StdEncoding.EncodeToString(pubkeyPem) 44 | 45 | err = mem.SetIDPublicKey(correlationID, secret, encoded) 46 | require.Nil(t, err, "could not set correlation-id and rsa public key in storage") 47 | 48 | item, ok := mem.cache.GetIfPresent(correlationID) 49 | require.True(t, ok, "could not assert item value presence") 50 | require.NotNil(t, item, "could not get correlation-id item from storage") 51 | 52 | value, ok := item.(*CorrelationData) 53 | require.True(t, ok, "could not assert item value type as correlation data") 54 | 55 | require.Equal(t, secret, value.SecretKey, "could not get correct secret key") 56 | } 57 | 58 | func TestStorageAddGetInteractions(t *testing.T) { 59 | mem, err := New(&Options{EvictionTTL: 1 * time.Hour}) 60 | require.Nil(t, err) 61 | 62 | secret := uuid.New().String() 63 | correlationID := xid.New().String() 64 | 65 | // Generate a 2048-bit private-key 66 | priv, err := rsa.GenerateKey(rand.Reader, 2048) 67 | require.Nil(t, err, "could not generate rsa key") 68 | 69 | pub := priv.Public() 70 | 71 | pubkeyBytes, err := x509.MarshalPKIXPublicKey(pub) 72 | require.Nil(t, err, "could not marshal public key") 73 | 74 | pubkeyPem := pem.EncodeToMemory(&pem.Block{ 75 | Type: "RSA PUBLIC KEY", 76 | Bytes: pubkeyBytes, 77 | }) 78 | 79 | encoded := base64.StdEncoding.EncodeToString(pubkeyPem) 80 | 81 | err = mem.SetIDPublicKey(correlationID, secret, encoded) 82 | require.Nil(t, err, "could not set correlation-id and rsa public key in storage") 83 | 84 | dataOriginal := []byte("hello world, this is unencrypted interaction") 85 | err = mem.AddInteraction(correlationID, dataOriginal) 86 | require.Nil(t, err, "could not add interaction to storage") 87 | 88 | data, key, err := mem.GetInteractions(correlationID, secret) 89 | require.Nil(t, err, "could not get interaction from storage") 90 | 91 | decodedKey, err := base64.StdEncoding.DecodeString(key) 92 | require.Nil(t, err, "could not decode key") 93 | 94 | // Decrypt the key plaintext first 95 | keyPlaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, priv, decodedKey, nil) 96 | require.Nil(t, err, "could not decrypt key to plaintext") 97 | 98 | cipherText, err := base64.StdEncoding.DecodeString(data[0]) 99 | require.Nil(t, err, "could not decode ciphertext") 100 | 101 | block, err := aes.NewCipher(keyPlaintext) 102 | require.Nil(t, err, "could not create aes cipher") 103 | 104 | if len(cipherText) < aes.BlockSize { 105 | require.Fail(t, "Cipher text is less than block size") 106 | } 107 | 108 | // IV is at the start of the Ciphertext 109 | iv := cipherText[:aes.BlockSize] 110 | cipherText = cipherText[aes.BlockSize:] 111 | 112 | // XORKeyStream can work in-place if the two arguments are the same. 113 | stream := cipher.NewCFBDecrypter(block, iv) 114 | decoded := make([]byte, len(cipherText)) 115 | stream.XORKeyStream(decoded, cipherText) 116 | 117 | require.Equal(t, dataOriginal, decoded, "could not get correct decrypted interaction") 118 | } 119 | 120 | func BenchmarkCacheParallelOther(b *testing.B) { 121 | cache := cache.New(cache.WithMaximumSize(DefaultOptions.MaxSize), cache.WithExpireAfterWrite(24*7*time.Hour)) 122 | 123 | b.RunParallel(func(pb *testing.PB) { 124 | for pb.Next() { 125 | doStuffWithOtherCache(cache) 126 | } 127 | }) 128 | } 129 | 130 | func doStuffWithOtherCache(cache cache.Cache) { 131 | for i := 0; i < 1e2; i++ { 132 | cache.Put(strconv.Itoa(i), "test") 133 | _, _ = cache.GetIfPresent(strconv.Itoa(i)) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/projectdiscovery/interactsh/pkg/server/acme" 8 | "github.com/projectdiscovery/interactsh/pkg/storage" 9 | stringsutil "github.com/projectdiscovery/utils/strings" 10 | ) 11 | 12 | // Interaction is an interaction received to the server. 13 | type Interaction struct { 14 | // Protocol for interaction, can contains HTTP/DNS/SMTP,etc. 15 | Protocol string `json:"protocol"` 16 | // UniqueID is the uniqueID for the subdomain receiving the interaction. 17 | UniqueID string `json:"unique-id"` 18 | // FullId is the full path for the subdomain receiving the interaction. 19 | FullId string `json:"full-id"` 20 | // QType is the question type for the interaction 21 | QType string `json:"q-type,omitempty"` 22 | // RawRequest is the raw request received by the interactsh server. 23 | RawRequest string `json:"raw-request,omitempty"` 24 | // RawResponse is the raw response sent by the interactsh server. 25 | RawResponse string `json:"raw-response,omitempty"` 26 | // SMTPFrom is the mail form field 27 | SMTPFrom string `json:"smtp-from,omitempty"` 28 | // RemoteAddress is the remote address for interaction 29 | RemoteAddress string `json:"remote-address"` 30 | // Timestamp is the timestamp for the interaction 31 | Timestamp time.Time `json:"timestamp"` 32 | AsnInfo []map[string]string `json:"asninfo,omitempty"` 33 | } 34 | 35 | // Options contains configuration options for the servers 36 | type Options struct { 37 | // Domains is the list domains for the instance. 38 | Domains []string 39 | // IPAddress is the IP address of the current server. 40 | IPAddress string 41 | // ListenIP is the IP address to listen servers on 42 | ListenIP string 43 | // DomainPort is the port to listen DNS servers on 44 | DnsPort int 45 | // HttpPort is the port to listen HTTP server on 46 | HttpPort int 47 | // HttpsPort is the port to listen HTTPS server on 48 | HttpsPort int 49 | // SmbPort is the port to listen Smb server on 50 | SmbPort int 51 | // SmtpPort is the port to listen Smtp server on 52 | SmtpPort int 53 | // SmtpsPort is the port to listen Smtps server on 54 | SmtpsPort int 55 | // SmtpAutoTLSPort is the port to listen Smtp autoTLS server on 56 | SmtpAutoTLSPort int 57 | // FtpPort is the port to listen Ftp server on 58 | FtpPort int 59 | // FtpPort is the port to listen Ftp server on 60 | LdapPort int 61 | // Hostmaster is the hostmaster email for the server. 62 | Hostmasters []string 63 | // Storage is a storage for interaction data storage 64 | Storage storage.Storage 65 | // Auth requires client to authenticate 66 | Auth bool 67 | // HTTPIndex is the http index file for server 68 | HTTPIndex string 69 | // HTTPDirectory is the directory for interact server 70 | HTTPDirectory string 71 | // Token required to retrieve interactions 72 | Token string 73 | // Enable root tld interactions 74 | RootTLD bool 75 | // OriginURL for the HTTP Server 76 | OriginURL string 77 | // FTPDirectory or temporary one 78 | FTPDirectory string 79 | // ScanEverywhere for potential correlation id 80 | ScanEverywhere bool 81 | // CorrelationIdLength of preamble 82 | CorrelationIdLength int 83 | // CorrelationIdNonceLength of the unique identifier 84 | CorrelationIdNonceLength int 85 | // Certificate Path 86 | CertificatePath string 87 | // Private Key Path 88 | PrivateKeyPath string 89 | // CustomRecords is a file containing custom DNS records 90 | CustomRecords string 91 | // HTTP header containing origin IP 92 | OriginIPHeader string 93 | // Version is the version of interactsh server 94 | Version string 95 | // DiskStorage enables storing interactions on disk 96 | DiskStorage bool 97 | // DiskStoragePath defines the disk storage location 98 | DiskStoragePath string 99 | // DynamicResp enables dynamic HTTP response 100 | DynamicResp bool 101 | // EnableMetrics enables metrics endpoint 102 | EnableMetrics bool 103 | // ServerToken hide server version in HTTP response X-Interactsh-Version header 104 | NoVersionHeader bool 105 | // HeaderServer use custom string in HTTP response Server header instead of domain 106 | HeaderServer string 107 | 108 | ACMEStore *acme.Provider 109 | Stats *Metrics 110 | OnResult OnResultCallback 111 | } 112 | type OnResultCallback func(out interface{}) 113 | 114 | func (options *Options) GetIdLength() int { 115 | return options.CorrelationIdLength + options.CorrelationIdNonceLength 116 | } 117 | 118 | // URLReflection returns a reversed part of the URL payload 119 | // which is checked in the response. 120 | func (options *Options) URLReflection(URL string) string { 121 | randomID := options.getURLIDComponent(URL) 122 | 123 | rns := []rune(randomID) 124 | for i, j := 0, len(rns)-1; i < j; i, j = i+1, j-1 { 125 | rns[i], rns[j] = rns[j], rns[i] 126 | } 127 | return string(rns) 128 | } 129 | 130 | // getURLIDComponent returns the interactsh ID 131 | func (options *Options) getURLIDComponent(URL string) string { 132 | parts := strings.Split(URL, ".") 133 | 134 | var randomID string 135 | for _, part := range parts { 136 | for scanChunk := range stringsutil.SlideWithLength(part, options.GetIdLength()) { 137 | if options.isCorrelationID(scanChunk) { 138 | randomID = part 139 | } 140 | } 141 | } 142 | 143 | return randomID 144 | } 145 | -------------------------------------------------------------------------------- /pkg/server/smtp_server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "fmt" 7 | "net" 8 | "strings" 9 | "sync/atomic" 10 | "time" 11 | 12 | "git.mills.io/prologic/smtpd" 13 | jsoniter "github.com/json-iterator/go" 14 | "github.com/projectdiscovery/gologger" 15 | stringsutil "github.com/projectdiscovery/utils/strings" 16 | ) 17 | 18 | // SMTPServer is a smtp server instance that listens both 19 | // TLS and Non-TLS based servers. 20 | type SMTPServer struct { 21 | options *Options 22 | smtpServer smtpd.Server 23 | smtpsServer smtpd.Server 24 | } 25 | 26 | // NewSMTPServer returns a new TLS & Non-TLS SMTP server. 27 | func NewSMTPServer(options *Options) (*SMTPServer, error) { 28 | server := &SMTPServer{options: options} 29 | 30 | authHandler := func(remoteAddr net.Addr, mechanism string, username []byte, password []byte, shared []byte) (bool, error) { 31 | return true, nil 32 | } 33 | rcptHandler := func(remoteAddr net.Addr, from string, to string) bool { 34 | return true 35 | } 36 | server.smtpServer = smtpd.Server{ 37 | Addr: fmt.Sprintf("%s:%d", options.ListenIP, options.SmtpPort), 38 | AuthHandler: authHandler, 39 | HandlerRcpt: rcptHandler, 40 | Hostname: options.Domains[0], 41 | Appname: "interactsh", 42 | Handler: smtpd.Handler(server.defaultHandler), 43 | } 44 | server.smtpsServer = smtpd.Server{ 45 | Addr: fmt.Sprintf("%s:%d", options.ListenIP, options.SmtpsPort), 46 | AuthHandler: authHandler, 47 | HandlerRcpt: rcptHandler, 48 | Hostname: options.Domains[0], 49 | Appname: "interactsh", 50 | Handler: smtpd.Handler(server.defaultHandler), 51 | } 52 | return server, nil 53 | } 54 | 55 | // ListenAndServe listens on smtp and/or smtps ports for the server. 56 | func (h *SMTPServer) ListenAndServe(tlsConfig *tls.Config, smtpAlive, smtpsAlive chan bool) { 57 | go func() { 58 | if tlsConfig == nil { 59 | return 60 | } 61 | srv := &smtpd.Server{Addr: fmt.Sprintf("%s:%d", h.options.ListenIP, h.options.SmtpAutoTLSPort), Handler: h.defaultHandler, Appname: "interactsh", Hostname: h.options.Domains[0]} 62 | srv.TLSConfig = tlsConfig 63 | 64 | smtpsAlive <- true 65 | err := srv.ListenAndServe() 66 | if err != nil { 67 | gologger.Error().Msgf("Could not serve smtp with tls on port %d: %s\n", h.options.SmtpAutoTLSPort, err) 68 | smtpsAlive <- false 69 | } 70 | }() 71 | 72 | smtpAlive <- true 73 | go func() { 74 | if err := h.smtpServer.ListenAndServe(); err != nil { 75 | smtpAlive <- false 76 | gologger.Error().Msgf("Could not serve smtp on port %d: %s\n", h.options.SmtpPort, err) 77 | } 78 | }() 79 | if err := h.smtpsServer.ListenAndServe(); err != nil { 80 | gologger.Error().Msgf("Could not serve smtp on port %d: %s\n", h.options.SmtpsPort, err) 81 | smtpAlive <- false 82 | } 83 | } 84 | 85 | // defaultHandler is a handler for default collaborator requests 86 | func (h *SMTPServer) defaultHandler(remoteAddr net.Addr, from string, to []string, data []byte) error { 87 | atomic.AddUint64(&h.options.Stats.Smtp, 1) 88 | 89 | var uniqueID, fullID string 90 | 91 | dataString := string(data) 92 | gologger.Debug().Msgf("New SMTP request: %s %s %s %s\n", remoteAddr, from, to, dataString) 93 | 94 | // if root-tld is enabled stores any interaction towards the main domain 95 | for _, addr := range to { 96 | if h.options.RootTLD { 97 | for _, domain := range h.options.Domains { 98 | if stringsutil.HasSuffixI(addr, domain) { 99 | ID := domain 100 | host, _, _ := net.SplitHostPort(remoteAddr.String()) 101 | address := addr[strings.LastIndex(addr, "@"):] 102 | interaction := &Interaction{ 103 | Protocol: "smtp", 104 | UniqueID: address, 105 | FullId: address, 106 | RawRequest: dataString, 107 | SMTPFrom: from, 108 | RemoteAddress: host, 109 | Timestamp: time.Now(), 110 | } 111 | buffer := &bytes.Buffer{} 112 | if err := jsoniter.NewEncoder(buffer).Encode(interaction); err != nil { 113 | gologger.Warning().Msgf("Could not encode root tld SMTP interaction: %s\n", err) 114 | } else { 115 | gologger.Debug().Msgf("Root TLD SMTP Interaction: \n%s\n", buffer.String()) 116 | if err := h.options.Storage.AddInteractionWithId(ID, buffer.Bytes()); err != nil { 117 | gologger.Warning().Msgf("Could not store root tld smtp interaction: %s\n", err) 118 | } 119 | } 120 | } 121 | } 122 | } 123 | } 124 | 125 | for _, addr := range to { 126 | if len(addr) > h.options.GetIdLength() && strings.Contains(addr, "@") { 127 | parts := strings.Split(addr[strings.LastIndex(addr, "@")+1:], ".") 128 | for i, part := range parts { 129 | if h.options.isCorrelationID(part) { 130 | uniqueID = part 131 | fullID = part 132 | if i+1 <= len(parts) { 133 | fullID = strings.Join(parts[:i+1], ".") 134 | } 135 | } 136 | } 137 | } 138 | } 139 | if uniqueID != "" { 140 | host, _, _ := net.SplitHostPort(remoteAddr.String()) 141 | 142 | correlationID := uniqueID[:h.options.CorrelationIdLength] 143 | interaction := &Interaction{ 144 | Protocol: "smtp", 145 | UniqueID: uniqueID, 146 | FullId: fullID, 147 | RawRequest: dataString, 148 | SMTPFrom: from, 149 | RemoteAddress: host, 150 | Timestamp: time.Now(), 151 | } 152 | buffer := &bytes.Buffer{} 153 | if err := jsoniter.NewEncoder(buffer).Encode(interaction); err != nil { 154 | gologger.Warning().Msgf("Could not encode smtp interaction: %s\n", err) 155 | } else { 156 | gologger.Debug().Msgf("%s\n", buffer.String()) 157 | if err := h.options.Storage.AddInteraction(correlationID, buffer.Bytes()); err != nil { 158 | gologger.Warning().Msgf("Could not store smtp interaction: %s\n", err) 159 | } 160 | } 161 | } 162 | return nil 163 | } 164 | -------------------------------------------------------------------------------- /pkg/server/acme/acme_certbot.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | "github.com/caddyserver/certmagic" 13 | "github.com/pkg/errors" 14 | "github.com/projectdiscovery/gologger" 15 | "go.uber.org/zap" 16 | ) 17 | 18 | // CleanupStorage perform cleanup routines tasks 19 | func CleanupStorage() { 20 | cleanupOptions := certmagic.CleanStorageOptions{OCSPStaples: true} 21 | certmagic.CleanStorage(context.Background(), certmagic.Default.Storage, cleanupOptions) 22 | } 23 | 24 | // HandleWildcardCertificates handles ACME wildcard cert generation with DNS 25 | // challenge using certmagic library from caddyserver. 26 | func HandleWildcardCertificates(domain, email string, store *Provider, debug bool) ([]tls.Certificate, error) { 27 | logger, err := zap.NewProduction() 28 | if err != nil { 29 | return nil, err 30 | } 31 | certmagic.DefaultACME.Agreed = true 32 | certmagic.DefaultACME.Email = email 33 | certmagic.DefaultACME.DNS01Solver = &certmagic.DNS01Solver{ 34 | DNSProvider: store, 35 | Resolvers: []string{ 36 | "8.8.8.8:53", 37 | "8.8.4.4:53", 38 | "1.1.1.1:53", 39 | "1.0.0.1:53", 40 | }, 41 | } 42 | originalDomain := strings.TrimPrefix(domain, "*.") 43 | 44 | certmagic.DefaultACME.CA = certmagic.LetsEncryptProductionCA 45 | if debug { 46 | certmagic.DefaultACME.Logger = logger 47 | } 48 | certmagic.DefaultACME.DisableHTTPChallenge = true 49 | certmagic.DefaultACME.DisableTLSALPNChallenge = true 50 | 51 | cfg := certmagic.NewDefault() 52 | if debug { 53 | cfg.Logger = logger 54 | } 55 | 56 | var creating bool 57 | if !certAlreadyExists(cfg, &certmagic.DefaultACME, domain) { 58 | creating = true 59 | gologger.Info().Msgf("Requesting SSL Certificate for: [%s, %s]", domain, originalDomain) 60 | } else { 61 | gologger.Info().Msgf("Loading existing SSL Certificate for: [%s, %s]", domain, originalDomain) 62 | } 63 | 64 | // this obtains certificates or renews them if necessary 65 | if syncErr := cfg.ObtainCertSync(context.Background(), domain); syncErr != nil { 66 | return nil, syncErr 67 | } 68 | 69 | domains := []string{domain, originalDomain} 70 | if syncErr := cfg.ManageSync(context.Background(), domains); syncErr != nil { 71 | gologger.Error().Msgf("Could not manage certmagic certs: %s", syncErr) 72 | } 73 | 74 | if creating { 75 | home, _ := os.UserHomeDir() 76 | gologger.Info().Msgf("Successfully Created SSL Certificate at: %s", filepath.Join(home, ".local", "share", "certmagic")) 77 | } 78 | 79 | // attempts to extract certificates from caddy 80 | var certs []tls.Certificate 81 | for _, domain := range domains { 82 | var retried, retriedWildcard bool 83 | retry_cert: 84 | certPath, privKeyPath, err := extractCaddyPaths(cfg, &certmagic.DefaultACME, domain) 85 | if err != nil { 86 | return nil, err 87 | } 88 | cert, err := tls.LoadX509KeyPair(certPath, privKeyPath) 89 | if err != nil { 90 | if !retried { 91 | retried = true 92 | // wait I/O to sync 93 | time.Sleep(5 * time.Second) 94 | goto retry_cert 95 | } 96 | if !retriedWildcard { 97 | retriedWildcard = true 98 | // wait I/O to sync 99 | time.Sleep(5 * time.Second) 100 | // attempt to load the domain as wildcard 101 | domain = fmt.Sprintf("wildcard_.%s", domain) 102 | goto retry_cert 103 | } 104 | } 105 | if err != nil { 106 | return nil, err 107 | } 108 | certs = append(certs, cert) 109 | } 110 | 111 | return certs, nil 112 | } 113 | 114 | // certAlreadyExists returns true if a cert already exists 115 | func certAlreadyExists(cfg *certmagic.Config, issuer certmagic.Issuer, domain string) bool { 116 | issuerKey := issuer.IssuerKey() 117 | certKey := certmagic.StorageKeys.SiteCert(issuerKey, domain) 118 | keyKey := certmagic.StorageKeys.SitePrivateKey(issuerKey, domain) 119 | metaKey := certmagic.StorageKeys.SiteMeta(issuerKey, domain) 120 | return cfg.Storage.Exists(context.Background(), certKey) && 121 | cfg.Storage.Exists(context.Background(), keyKey) && 122 | cfg.Storage.Exists(context.Background(), metaKey) 123 | } 124 | 125 | // extractCaddyPaths attempts to extract cert and private key through the layers of abstractions from the domain name 126 | func extractCaddyPaths(cfg *certmagic.Config, issuer certmagic.Issuer, domain string) (certPath, privKeyPath string, err error) { 127 | issuerKey := issuer.IssuerKey() 128 | certId := certmagic.StorageKeys.SiteCert(issuerKey, domain) 129 | keyId := certmagic.StorageKeys.SitePrivateKey(issuerKey, domain) 130 | // we need to coerce the storage to file system one to be able to obtain access to the typed methods 131 | if cfgStorage, ok := cfg.Storage.(*certmagic.FileStorage); ok { 132 | certPath = cfgStorage.Filename(certId) 133 | privKeyPath = cfgStorage.Filename(keyId) 134 | } 135 | if certPath != "" && privKeyPath != "" { 136 | return 137 | } 138 | err = errors.New("couldn't extract cert and private key paths") 139 | return 140 | } 141 | 142 | // BuildTlsConfigWithCertAndKeyPaths Build TlsConfig with certificates 143 | func BuildTlsConfigWithCertAndKeyPaths(certPath, privKeyPath, domain string) (*tls.Config, error) { 144 | cert, err := tls.LoadX509KeyPair(certPath, privKeyPath) 145 | if err != nil { 146 | return nil, errors.New("Could not load certs and private key") 147 | } 148 | return BuildTlsConfigWithCerts(domain, cert) 149 | } 150 | 151 | // BuildTlsConfigWithCerts Build TlsConfig with existing certificates 152 | func BuildTlsConfigWithCerts(domain string, certs ...tls.Certificate) (*tls.Config, error) { 153 | tlsConfig := &tls.Config{ 154 | InsecureSkipVerify: true, 155 | Certificates: certs, 156 | } 157 | if domain != "" { 158 | tlsConfig.ServerName = domain 159 | } 160 | tlsConfig.NextProtos = []string{"h2", "http/1.1"} 161 | return tlsConfig, nil 162 | } 163 | -------------------------------------------------------------------------------- /deploy/agent.yaml.j2: -------------------------------------------------------------------------------- 1 | metrics: 2 | wal_directory: /tmp/agent 3 | global: 4 | scrape_interval: 30s 5 | external_labels: 6 | env: "{{ env_name }}" 7 | remote_write: 8 | - url: {{ prometheus_cloud_url }} 9 | basic_auth: 10 | password: {{ prometheus_cloud_password | trim }} 11 | username: {{ prometheus_cloud_username }} 12 | 13 | logs: 14 | positions_directory: /tmp/positions/ 15 | configs: 16 | - name: "{{ domain_name }}" 17 | clients: 18 | - url: {{ grafana_cloud_url }} 19 | external_labels: {"server_name" : "{{ domain_name }}"} 20 | scrape_configs: 21 | - job_name: flog_scrape 22 | docker_sd_configs: 23 | - host: unix:///var/run/docker.sock 24 | refresh_interval: 5s 25 | filters: 26 | - name: name 27 | values: ["{{ container_name }}"] 28 | relabel_configs: 29 | - source_labels: ['__meta_docker_container_name'] 30 | regex: '/(.*)' 31 | target_label: 'container' 32 | replacement: "{{ domain_name }}" 33 | 34 | integrations: 35 | node_exporter: 36 | enabled: true 37 | instance: "{{ domain_name }}" 38 | rootfs_path: /rootfs 39 | sysfs_path: /sys 40 | procfs_path: /host/proc 41 | metric_relabel_configs: 42 | - source_labels: [__name__] 43 | regex: '(node_arp_entries|node_context_switches_total|node_cooling_device_cur_state|node_cooling_device_max_state|node_cpu_guest_seconds_total|node_cpu_seconds_total|node_disk_discards_completed_total|node_disk_discards_merged_total|node_disk_discard_time_seconds_total|node_disk_io_now|node_disk_io_time_seconds_total|node_disk_io_time_weighted_seconds_total|node_disk_read_bytes_total|node_disk_reads_completed_total|node_disk_reads_merged_total|node_disk_read_time_seconds_total|node_disk_writes_completed_total|node_disk_writes_merged_total|node_disk_write_time_seconds_total|node_disk_written_bytes_total|node_entropy_available_bits|node_filefd_allocated|node_filefd_maximum|node_filesystem_avail_bytes|node_filesystem_device_error|node_filesystem_files|node_filesystem_files_free|node_filesystem_free_bytes|node_filesystem_readonly|node_filesystem_size_bytes|node_forks_total|node_hwmon_temp_celsius|node_hwmon_temp_crit_alarm_celsius|node_hwmon_temp_crit_celsius|node_hwmon_temp_crit_hyst_celsius|node_hwmon_temp_max_celsius|node_interrupts_total|node_intr_total|node_load1|node_load15|node_load5|node_memory_Active_anon_bytes|node_memory_Active_bytes|node_memory_Active_file_bytes|node_memory_AnonHugePages_bytes|node_memory_AnonPages_bytes|node_memory_Bounce_bytes|node_memory_Buffers_bytes|node_memory_Cached_bytes|node_memory_CommitLimit_bytes|node_memory_Committed_AS_bytes|node_memory_DirectMap1G_bytes|node_memory_DirectMap2M_bytes|node_memory_DirectMap4k_bytes|node_memory_Dirty_bytes|node_memory_HardwareCorrupted_bytes|node_memory_HugePages_Free|node_memory_Hugepagesize_bytes|node_memory_HugePages_Rsvd|node_memory_HugePages_Surp|node_memory_HugePages_Total|node_memory_Inactive_anon_bytes|node_memory_Inactive_bytes|node_memory_Inactive_file_bytes|node_memory_KernelStack_bytes|node_memory_Mapped_bytes|node_memory_MemAvailable_bytes|node_memory_MemFree_bytes|node_memory_MemTotal_bytes|node_memory_Mlocked_bytes|node_memory_NFS_Unstable_bytes|node_memory_PageTables_bytes|node_memory_Percpu_bytes|node_memory_Shmem_bytes|node_memory_ShmemHugePages_bytes|node_memory_ShmemPmdMapped_bytes|node_memory_Slab_bytes|node_memory_SReclaimable_bytes|node_memory_SUnreclaim_bytes|node_memory_SwapCached_bytes|node_memory_SwapTotal_bytes|node_memory_Unevictable_bytes|node_memory_VmallocChunk_bytes|node_memory_VmallocTotal_bytes|node_memory_VmallocUsed_bytes|node_memory_Writeback_bytes|node_memory_WritebackTmp_bytes|node_netstat_Icmp_InErrors|node_netstat_Icmp_InMsgs|node_netstat_Icmp_OutMsgs|node_netstat_IpExt_InOctets|node_netstat_IpExt_OutOctets|node_netstat_Ip_Forwarding|node_netstat_Tcp_ActiveOpens|node_netstat_Tcp_CurrEstab|node_netstat_TcpExt_ListenDrops|node_netstat_TcpExt_ListenOverflows|node_netstat_TcpExt_SyncookiesFailed|node_netstat_TcpExt_SyncookiesRecv|node_netstat_TcpExt_SyncookiesSent|node_netstat_TcpExt_TCPSynRetrans|node_netstat_Tcp_InErrs|node_netstat_Tcp_InSegs|node_netstat_Tcp_MaxConn|node_netstat_Tcp_OutRsts|node_netstat_Tcp_OutSegs|node_netstat_Tcp_PassiveOpens|node_netstat_Tcp_RetransSegs|node_netstat_Udp_InDatagrams|node_netstat_Udp_InErrors|node_netstat_UdpLite_InErrors|node_netstat_Udp_NoPorts|node_netstat_Udp_OutDatagrams|node_netstat_Udp_RcvbufErrors|node_netstat_Udp_SndbufErrors|node_network_carrier|node_network_mtu_bytes|node_network_receive_bytes_total|node_network_receive_compressed_total|node_network_receive_drop_total|node_network_receive_errs_total|node_network_receive_fifo_total|node_network_receive_frame_total|node_network_receive_multicast_total|node_network_receive_packets_total|node_network_speed_bytes|node_network_transmit_bytes_total|node_network_transmit_carrier_total|node_network_transmit_colls_total|node_network_transmit_compressed_total|node_network_transmit_drop_total|node_network_transmit_errs_total|node_network_transmit_fifo_total|node_network_transmit_packets_total|node_network_transmit_queue_length|node_network_up|node_nf_conntrack_entries|node_nf_conntrack_entries_limit|node_power_supply_online|node_processes_max_processes|node_processes_max_threads|node_processes_pids|node_processes_state|node_processes_threads|node_procs_blocked|node_procs_running|node_schedstat_running_seconds_total|node_schedstat_timeslices_total|node_schedstat_waiting_seconds_total|node_scrape_collector_duration_seconds|node_scrape_collector_success|node_sockstat_FRAG_inuse|node_sockstat_FRAG_memory|node_sockstat_RAW_inuse|node_sockstat_sockets_used|node_sockstat_TCP_alloc|node_sockstat_TCP_inuse|node_sockstat_TCP_mem|node_sockstat_TCP_mem_bytes|node_sockstat_TCP_orphan|node_sockstat_TCP_tw|node_sockstat_UDP_inuse|node_sockstat_UDPLITE_inuse|node_sockstat_UDP_mem|node_sockstat_UDP_mem_bytes|node_softnet_dropped_total|node_softnet_processed_total|node_softnet_times_squeezed_total|node_systemd_socket_accepted_connections_total|node_systemd_units|node_textfile_scrape_error|node_time_seconds|node_timex_estimated_error_seconds|node_timex_frequency_adjustment_ratio|node_timex_loop_time_constant|node_timex_maxerror_seconds|node_timex_offset_seconds|node_timex_sync_status|node_timex_tai_offset_seconds|node_timex_tick_seconds|node_vmstat_oom_kill|node_vmstat_pgfault|node_vmstat_pgmajfault|node_vmstat_pgpgin|node_vmstat_pgpgout|node_vmstat_pswpin|node_vmstat_pswpout|process_cpu_seconds_total|process_max_fds|process_open_fds|process_resident_memory_max_bytes|process_virtual_memory_bytes|process_virtual_memory_max_bytes)' 44 | action: keep 45 | - source_labels: [__name__] 46 | regex: '(^go_.*|^promhttp_metric_.*)' 47 | action: drop 48 | cadvisor: 49 | enabled: true 50 | docker_only: true 51 | instance: "{{ domain_name }}" 52 | disabled_metrics: 53 | - disk 54 | enabled_metrics: 55 | - memory -------------------------------------------------------------------------------- /pkg/server/ftp_server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strings" 10 | "sync/atomic" 11 | "time" 12 | 13 | jsoniter "github.com/json-iterator/go" 14 | "github.com/projectdiscovery/gologger" 15 | ftpserver "goftp.io/server/v2" 16 | "goftp.io/server/v2/driver/file" 17 | ) 18 | 19 | // FTPServer is a ftp server instance 20 | type FTPServer struct { 21 | options *Options 22 | ftpServer *ftpserver.Server 23 | } 24 | 25 | // NewFTPServer returns a new TLS & Non-TLS FTP server. 26 | func NewFTPServer(options *Options) (*FTPServer, error) { 27 | server := &FTPServer{options: options} 28 | 29 | ftpFolder := options.FTPDirectory 30 | if ftpFolder == "" { 31 | var err error 32 | ftpFolder, err = os.MkdirTemp("", "") 33 | if err != nil { 34 | return nil, err 35 | } 36 | } 37 | 38 | driver, err := file.NewDriver(ftpFolder) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | nopDriver := NewNopDriver(driver) 44 | 45 | opt := &ftpserver.Options{ 46 | Name: "interactsh-ftp", 47 | Driver: nopDriver, 48 | Port: options.FtpPort, 49 | Perm: ftpserver.NewSimplePerm("root", "root"), 50 | Logger: server, 51 | Auth: &NopAuth{}, 52 | } 53 | 54 | // start ftp server 55 | ftpServer, err := ftpserver.NewServer(opt) 56 | if err != nil { 57 | return nil, err 58 | } 59 | server.ftpServer = ftpServer 60 | ftpServer.RegisterNotifer(server) 61 | 62 | return server, nil 63 | } 64 | 65 | // ListenAndServe listens on smtp and/or smtps ports for the server. 66 | func (h *FTPServer) ListenAndServe(tlsConfig *tls.Config, ftpAlive chan bool) { 67 | ftpAlive <- true 68 | if err := h.ftpServer.ListenAndServe(); err != nil { 69 | gologger.Error().Msgf("Could not serve ftp on port 21: %s\n", err) 70 | ftpAlive <- false 71 | } 72 | } 73 | 74 | func (h *FTPServer) Close() { 75 | _ = h.ftpServer.Shutdown() 76 | } 77 | 78 | func (h *FTPServer) recordInteraction(remoteAddress, data string) { 79 | atomic.AddUint64(&h.options.Stats.Ftp, 1) 80 | 81 | if data == "" { 82 | return 83 | } 84 | interaction := &Interaction{ 85 | RemoteAddress: remoteAddress, 86 | Protocol: "ftp", 87 | RawRequest: data, 88 | Timestamp: time.Now(), 89 | } 90 | buffer := &bytes.Buffer{} 91 | if err := jsoniter.NewEncoder(buffer).Encode(interaction); err != nil { 92 | gologger.Warning().Msgf("Could not encode ftp interaction: %s\n", err) 93 | } else { 94 | gologger.Debug().Msgf("FTP Interaction: \n%s\n", buffer.String()) 95 | if err := h.options.Storage.AddInteractionWithId(h.options.Token, buffer.Bytes()); err != nil { 96 | gologger.Warning().Msgf("Could not store ftp interaction: %s\n", err) 97 | } 98 | } 99 | } 100 | 101 | func (h *FTPServer) Print(sessionID string, message interface{}) {} 102 | func (h *FTPServer) Printf(sessionID string, format string, v ...interface{}) {} 103 | func (h *FTPServer) PrintCommand(sessionID string, command string, params string) { 104 | h.Print(sessionID, fmt.Sprintf("%s %s", command, params)) 105 | } 106 | func (h *FTPServer) PrintResponse(sessionID string, code int, message string) { 107 | h.Print(sessionID, fmt.Sprintf("%d %s", code, message)) 108 | } 109 | 110 | func (h *FTPServer) BeforeLoginUser(ctx *ftpserver.Context, userName string) { 111 | var b strings.Builder 112 | b.WriteString(ctx.Cmd) 113 | b.WriteString(" ") 114 | b.WriteString(ctx.Param) 115 | b.WriteString("\n") 116 | b.WriteString(userName + " logging in") 117 | h.recordInteraction(ctx.Sess.RemoteAddr().String(), b.String()) 118 | } 119 | func (h *FTPServer) BeforePutFile(ctx *ftpserver.Context, dstPath string) { 120 | var b strings.Builder 121 | b.WriteString(ctx.Cmd) 122 | b.WriteString(" ") 123 | b.WriteString(ctx.Param) 124 | b.WriteString("\n") 125 | b.WriteString("uploading " + dstPath) 126 | h.recordInteraction(ctx.Sess.RemoteAddr().String(), b.String()) 127 | } 128 | func (h *FTPServer) BeforeDeleteFile(ctx *ftpserver.Context, dstPath string) { 129 | var b strings.Builder 130 | b.WriteString(ctx.Cmd) 131 | b.WriteString(" ") 132 | b.WriteString(ctx.Param) 133 | b.WriteString("\n") 134 | b.WriteString("deleting " + dstPath) 135 | h.recordInteraction(ctx.Sess.RemoteAddr().String(), b.String()) 136 | } 137 | func (h *FTPServer) BeforeChangeCurDir(ctx *ftpserver.Context, oldCurDir, newCurDir string) { 138 | var b strings.Builder 139 | b.WriteString(ctx.Cmd) 140 | b.WriteString(" ") 141 | b.WriteString(ctx.Param) 142 | b.WriteString("\n") 143 | b.WriteString("changing directory from " + oldCurDir + " to " + newCurDir) 144 | h.recordInteraction(ctx.Sess.RemoteAddr().String(), b.String()) 145 | } 146 | func (h *FTPServer) BeforeCreateDir(ctx *ftpserver.Context, dstPath string) { 147 | var b strings.Builder 148 | b.WriteString(ctx.Cmd) 149 | b.WriteString(" ") 150 | b.WriteString(ctx.Param) 151 | b.WriteString("\n") 152 | b.WriteString("creating directory " + dstPath) 153 | h.recordInteraction(ctx.Sess.RemoteAddr().String(), b.String()) 154 | } 155 | func (h *FTPServer) BeforeDeleteDir(ctx *ftpserver.Context, dstPath string) { 156 | var b strings.Builder 157 | b.WriteString(ctx.Cmd) 158 | b.WriteString(" ") 159 | b.WriteString(ctx.Param) 160 | b.WriteString("\n") 161 | b.WriteString("deleting directory " + dstPath) 162 | h.recordInteraction(ctx.Sess.RemoteAddr().String(), b.String()) 163 | } 164 | func (h *FTPServer) BeforeDownloadFile(ctx *ftpserver.Context, dstPath string) { 165 | var b strings.Builder 166 | b.WriteString(ctx.Cmd) 167 | b.WriteString(" ") 168 | b.WriteString(ctx.Param) 169 | b.WriteString("\n") 170 | b.WriteString("downloading file " + dstPath) 171 | h.recordInteraction(ctx.Sess.RemoteAddr().String(), b.String()) 172 | } 173 | func (h *FTPServer) AfterUserLogin(ctx *ftpserver.Context, userName, password string, passMatched bool, err error) { 174 | var b strings.Builder 175 | b.WriteString(ctx.Cmd) 176 | b.WriteString(" ") 177 | b.WriteString(ctx.Param) 178 | b.WriteString("\n") 179 | b.WriteString("user " + userName + " logged in with password " + password) 180 | h.recordInteraction(ctx.Sess.RemoteAddr().String(), b.String()) 181 | } 182 | func (h *FTPServer) AfterFilePut(ctx *ftpserver.Context, dstPath string, size int64, err error) { 183 | var b strings.Builder 184 | b.WriteString(ctx.Cmd) 185 | b.WriteString(" ") 186 | b.WriteString(ctx.Param) 187 | b.WriteString("\n") 188 | b.WriteString("uploaded " + dstPath) 189 | h.recordInteraction(ctx.Sess.RemoteAddr().String(), b.String()) 190 | } 191 | func (h *FTPServer) AfterFileDeleted(ctx *ftpserver.Context, dstPath string, err error) { 192 | var b strings.Builder 193 | b.WriteString(ctx.Cmd) 194 | b.WriteString(" ") 195 | b.WriteString(ctx.Param) 196 | b.WriteString("\n") 197 | b.WriteString("deleted " + dstPath) 198 | h.recordInteraction(ctx.Sess.RemoteAddr().String(), b.String()) 199 | } 200 | func (h *FTPServer) AfterFileDownloaded(ctx *ftpserver.Context, dstPath string, size int64, err error) { 201 | var b strings.Builder 202 | b.WriteString(ctx.Cmd) 203 | b.WriteString(" ") 204 | b.WriteString(ctx.Param) 205 | b.WriteString("\n") 206 | b.WriteString("downloaded file " + dstPath) 207 | h.recordInteraction(ctx.Sess.RemoteAddr().String(), b.String()) 208 | } 209 | func (h *FTPServer) AfterCurDirChanged(ctx *ftpserver.Context, oldCurDir, newCurDir string, err error) { 210 | var b strings.Builder 211 | b.WriteString(ctx.Cmd) 212 | b.WriteString(" ") 213 | b.WriteString(ctx.Param) 214 | b.WriteString("\n") 215 | b.WriteString("changed directory from " + oldCurDir + " to " + newCurDir) 216 | h.recordInteraction(ctx.Sess.RemoteAddr().String(), b.String()) 217 | } 218 | func (h *FTPServer) AfterDirCreated(ctx *ftpserver.Context, dstPath string, err error) { 219 | var b strings.Builder 220 | b.WriteString(ctx.Cmd) 221 | b.WriteString(" ") 222 | b.WriteString(ctx.Param) 223 | b.WriteString("\n") 224 | b.WriteString("created directory " + dstPath) 225 | h.recordInteraction(ctx.Sess.RemoteAddr().String(), b.String()) 226 | } 227 | func (h *FTPServer) AfterDirDeleted(ctx *ftpserver.Context, dstPath string, err error) { 228 | var b strings.Builder 229 | b.WriteString(ctx.Cmd) 230 | b.WriteString(" ") 231 | b.WriteString(ctx.Param) 232 | b.WriteString("\n") 233 | b.WriteString("delete directory " + dstPath) 234 | h.recordInteraction(ctx.Sess.RemoteAddr().String(), b.String()) 235 | } 236 | 237 | type NopAuth struct{} 238 | 239 | func (a *NopAuth) CheckPasswd(ctx *ftpserver.Context, name, pass string) (bool, error) { 240 | return true, nil 241 | } 242 | 243 | type NopDriver struct { 244 | driver ftpserver.Driver 245 | } 246 | 247 | func NewNopDriver(driver ftpserver.Driver) *NopDriver { 248 | return &NopDriver{driver: driver} 249 | } 250 | 251 | func (n *NopDriver) Stat(c *ftpserver.Context, s string) (os.FileInfo, error) { 252 | return n.driver.Stat(c, s) 253 | } 254 | 255 | func (n *NopDriver) ListDir(c *ftpserver.Context, s string, f func(os.FileInfo) error) error { 256 | return n.driver.ListDir(c, s, f) 257 | } 258 | 259 | func (n *NopDriver) DeleteDir(c *ftpserver.Context, s string) error { 260 | return nil 261 | } 262 | 263 | func (n *NopDriver) DeleteFile(c *ftpserver.Context, s string) error { 264 | return nil 265 | } 266 | 267 | func (n *NopDriver) Rename(c *ftpserver.Context, s1 string, s2 string) error { 268 | return nil 269 | } 270 | 271 | func (n *NopDriver) MakeDir(c *ftpserver.Context, s string) error { 272 | return nil 273 | } 274 | 275 | func (n *NopDriver) GetFile(c *ftpserver.Context, s1 string, k int64) (int64, io.ReadCloser, error) { 276 | return n.driver.GetFile(c, s1, k) 277 | } 278 | 279 | func (n *NopDriver) PutFile(c *ftpserver.Context, s string, r io.Reader, k int64) (int64, error) { 280 | return k, nil 281 | } 282 | -------------------------------------------------------------------------------- /pkg/storage/storagedb.go: -------------------------------------------------------------------------------- 1 | // storage implements a encrypted memory mechanism 2 | package storage 3 | 4 | import ( 5 | "bytes" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/sha256" 9 | "encoding/base64" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/goburrow/cache" 15 | "github.com/google/uuid" 16 | "github.com/pkg/errors" 17 | fileutil "github.com/projectdiscovery/utils/file" 18 | "github.com/rs/xid" 19 | "github.com/syndtr/goleveldb/leveldb" 20 | "github.com/syndtr/goleveldb/leveldb/opt" 21 | "go.uber.org/multierr" 22 | ) 23 | 24 | // Storage is an storage for interactsh interaction data as well 25 | // as correlation-id -> rsa-public-key data. 26 | type StorageDB struct { 27 | Options *Options 28 | cache cache.Cache 29 | db *leveldb.DB 30 | dbpath string 31 | } 32 | 33 | // New creates a new storage instance for interactsh data. 34 | func New(options *Options) (*StorageDB, error) { 35 | storageDB := &StorageDB{Options: options} 36 | cacheOptions := []cache.Option{ 37 | cache.WithMaximumSize(options.MaxSize), 38 | } 39 | if options.EvictionTTL > 0 { 40 | cacheOptions = append(cacheOptions, cache.WithExpireAfterAccess(options.EvictionTTL)) 41 | } 42 | if options.UseDisk() { 43 | cacheOptions = append(cacheOptions, cache.WithRemovalListener(storageDB.OnCacheRemovalCallback)) 44 | } 45 | cacheDb := cache.New(cacheOptions...) 46 | storageDB.cache = cacheDb 47 | 48 | if options.UseDisk() { 49 | // if the path exists we create a random temporary subfolder 50 | if !fileutil.FolderExists(options.DbPath) { 51 | return nil, errors.New("folder doesn't exist") 52 | } 53 | dbpath := filepath.Join(options.DbPath, xid.New().String()) 54 | 55 | if err := os.MkdirAll(dbpath, 0644); err != nil { 56 | return nil, err 57 | } 58 | levDb, err := leveldb.OpenFile(dbpath, &opt.Options{}) 59 | if err != nil { 60 | return nil, err 61 | } 62 | storageDB.dbpath = dbpath 63 | storageDB.db = levDb 64 | } 65 | 66 | return storageDB, nil 67 | } 68 | 69 | func (s *StorageDB) OnCacheRemovalCallback(key cache.Key, value cache.Value) { 70 | if key, ok := value.([]byte); ok { 71 | _ = s.db.Delete(key, &opt.WriteOptions{}) 72 | } 73 | } 74 | 75 | func (s *StorageDB) GetCacheMetrics() (*CacheMetrics, error) { 76 | info := &cache.Stats{} 77 | s.cache.Stats(info) 78 | 79 | cacheMetrics := &CacheMetrics{ 80 | HitCount: info.HitCount, 81 | MissCount: info.MissCount, 82 | LoadSuccessCount: info.LoadSuccessCount, 83 | LoadErrorCount: info.LoadErrorCount, 84 | TotalLoadTime: info.TotalLoadTime, 85 | EvictionCount: info.EvictionCount, 86 | } 87 | 88 | return cacheMetrics, nil 89 | } 90 | 91 | // SetIDPublicKey sets the correlation ID and publicKey into the cache for further operations. 92 | func (s *StorageDB) SetIDPublicKey(correlationID, secretKey, publicKey string) error { 93 | // If we already have this correlation ID, return. 94 | _, found := s.cache.GetIfPresent(correlationID) 95 | if found { 96 | return errors.New("correlation-id provided already exists") 97 | } 98 | publicKeyData, err := ParseB64RSAPublicKeyFromPEM(publicKey) 99 | if err != nil { 100 | return errors.Wrap(err, "could not read public Key") 101 | } 102 | aesKey := uuid.New().String()[:32] 103 | 104 | ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKeyData, []byte(aesKey), []byte("")) 105 | if err != nil { 106 | return errors.New("could not encrypt event data") 107 | } 108 | 109 | data := &CorrelationData{ 110 | SecretKey: secretKey, 111 | AESKey: []byte(aesKey), 112 | AESKeyEncrypted: base64.StdEncoding.EncodeToString(ciphertext), 113 | } 114 | s.cache.Put(correlationID, data) 115 | return nil 116 | } 117 | 118 | func (s *StorageDB) SetID(ID string) error { 119 | data := &CorrelationData{} 120 | s.cache.Put(ID, data) 121 | return nil 122 | } 123 | 124 | // AddInteraction adds an interaction data to the correlation ID after encrypting 125 | // it with Public Key for the provided correlation ID. 126 | func (s *StorageDB) AddInteraction(correlationID string, data []byte) error { 127 | item, found := s.cache.GetIfPresent(correlationID) 128 | if !found { 129 | return ErrCorrelationIdNotFound 130 | } 131 | value, ok := item.(*CorrelationData) 132 | if !ok { 133 | return errors.New("invalid correlation-id cache value found") 134 | } 135 | 136 | if s.Options.UseDisk() { 137 | ct, err := AESEncrypt(value.AESKey, data) 138 | if err != nil { 139 | return errors.Wrap(err, "could not encrypt event data") 140 | } 141 | value.Lock() 142 | existingData, _ := s.db.Get([]byte(correlationID), nil) 143 | _ = s.db.Put([]byte(correlationID), AppendMany("\n", existingData, []byte(ct)), nil) 144 | value.Unlock() 145 | } else { 146 | value.Lock() 147 | value.Data = append(value.Data, string(data)) 148 | value.Unlock() 149 | } 150 | 151 | return nil 152 | } 153 | 154 | // AddInteractionWithId adds an interaction data to the id bucket 155 | func (s *StorageDB) AddInteractionWithId(id string, data []byte) error { 156 | item, ok := s.cache.GetIfPresent(id) 157 | if !ok { 158 | return ErrCorrelationIdNotFound 159 | } 160 | value, ok := item.(*CorrelationData) 161 | if !ok { 162 | return errors.New("invalid correlation-id cache value found") 163 | } 164 | 165 | if s.Options.UseDisk() { 166 | ct, err := AESEncrypt(value.AESKey, data) 167 | if err != nil { 168 | return errors.Wrap(err, "could not encrypt event data") 169 | } 170 | value.Lock() 171 | existingData, _ := s.db.Get([]byte(id), nil) 172 | _ = s.db.Put([]byte(id), AppendMany("\n", existingData, []byte(ct)), nil) 173 | value.Unlock() 174 | } else { 175 | value.Lock() 176 | value.Data = append(value.Data, string(data)) 177 | value.Unlock() 178 | } 179 | 180 | return nil 181 | } 182 | 183 | // GetInteractions returns the interactions for a correlationID and removes 184 | // it from the storage. It also returns AES Encrypted Key for the IDs. 185 | func (s *StorageDB) GetInteractions(correlationID, secret string) ([]string, string, error) { 186 | item, ok := s.cache.GetIfPresent(correlationID) 187 | if !ok { 188 | return nil, "", ErrCorrelationIdNotFound 189 | } 190 | value, ok := item.(*CorrelationData) 191 | if !ok { 192 | return nil, "", errors.New("invalid correlation-id cache value found") 193 | } 194 | if !strings.EqualFold(value.SecretKey, secret) { 195 | return nil, "", errors.New("invalid secret key passed for user") 196 | } 197 | data, err := s.getInteractions(value, correlationID) 198 | return data, value.AESKeyEncrypted, err 199 | } 200 | 201 | // GetInteractions returns the interactions for a id and empty the cache 202 | func (s *StorageDB) GetInteractionsWithId(id string) ([]string, error) { 203 | item, ok := s.cache.GetIfPresent(id) 204 | if !ok { 205 | return nil, errors.New("could not get id from cache") 206 | } 207 | value, ok := item.(*CorrelationData) 208 | if !ok { 209 | return nil, errors.New("invalid id cache value found") 210 | } 211 | return s.getInteractions(value, id) 212 | } 213 | 214 | // RemoveID removes data for a correlation ID and data related to it. 215 | func (s *StorageDB) RemoveID(correlationID, secret string) error { 216 | item, ok := s.cache.GetIfPresent(correlationID) 217 | if !ok { 218 | return ErrCorrelationIdNotFound 219 | } 220 | value, ok := item.(*CorrelationData) 221 | if !ok { 222 | return errors.New("invalid correlation-id cache value found") 223 | } 224 | if !strings.EqualFold(value.SecretKey, secret) { 225 | return errors.New("invalid secret key passed for deregister") 226 | } 227 | value.Lock() 228 | value.Data = nil 229 | value.Unlock() 230 | s.cache.Invalidate(correlationID) 231 | 232 | if s.Options.UseDisk() { 233 | return s.db.Delete([]byte(correlationID), nil) 234 | } 235 | return nil 236 | } 237 | 238 | // GetCacheItem returns an item as is 239 | func (s *StorageDB) GetCacheItem(token string) (*CorrelationData, error) { 240 | item, ok := s.cache.GetIfPresent(token) 241 | if !ok { 242 | return nil, errors.New("cache item not found") 243 | } 244 | value, ok := item.(*CorrelationData) 245 | if !ok { 246 | return nil, errors.New("cache item not found") 247 | } 248 | return value, nil 249 | } 250 | 251 | func (s *StorageDB) getInteractions(correlationData *CorrelationData, id string) ([]string, error) { 252 | correlationData.Lock() 253 | defer correlationData.Unlock() 254 | 255 | switch { 256 | case s.Options.UseDisk(): 257 | data, err := s.db.Get([]byte(id), nil) 258 | if err != nil { 259 | if errors.Is(err, leveldb.ErrNotFound) { 260 | err = nil 261 | } 262 | return nil, err 263 | } 264 | var dataString []string 265 | for _, d := range bytes.Split(data, []byte("\n")) { 266 | dataString = append(dataString, string(d)) 267 | } 268 | _ = s.db.Delete([]byte(id), nil) 269 | return dataString, nil 270 | default: 271 | // in memory data 272 | var errs []error 273 | data := correlationData.Data 274 | correlationData.Data = nil 275 | if len(data) == 0 { 276 | return nil, nil 277 | } 278 | 279 | for i, dataItem := range data { 280 | encryptedDataItem, err := AESEncrypt(correlationData.AESKey, []byte(dataItem)) 281 | if err != nil { 282 | errs = append(errs, errors.Wrap(err, "could not encrypt event data")) 283 | data[i] = dataItem 284 | } else { 285 | data[i] = encryptedDataItem 286 | } 287 | } 288 | return data, multierr.Combine(errs...) 289 | } 290 | } 291 | 292 | func (s *StorageDB) Close() error { 293 | var errdbClosed error 294 | if s.db != nil { 295 | errdbClosed = s.db.Close() 296 | } 297 | return multierr.Combine( 298 | s.cache.Close(), 299 | errdbClosed, 300 | os.RemoveAll(s.dbpath), 301 | ) 302 | } 303 | -------------------------------------------------------------------------------- /pkg/server/dns_server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "net" 8 | "os" 9 | "strings" 10 | "sync/atomic" 11 | "time" 12 | 13 | jsoniter "github.com/json-iterator/go" 14 | "github.com/miekg/dns" 15 | "github.com/pkg/errors" 16 | "github.com/projectdiscovery/gologger" 17 | "github.com/projectdiscovery/interactsh/pkg/server/acme" 18 | stringsutil "github.com/projectdiscovery/utils/strings" 19 | "gopkg.in/yaml.v3" 20 | ) 21 | 22 | // DNSServer is a DNS server instance that listens on port 53. 23 | type DNSServer struct { 24 | options *Options 25 | mxDomains map[string]string 26 | nsDomains map[string][]string 27 | ipAddress net.IP 28 | timeToLive uint32 29 | server *dns.Server 30 | customRecords *customDNSRecords 31 | TxtRecord string // used for ACME verification 32 | } 33 | 34 | // NewDNSServer returns a new DNS server. 35 | func NewDNSServer(network string, options *Options) *DNSServer { 36 | mxDomains := make(map[string]string) 37 | nsDomains := make(map[string][]string) 38 | 39 | for _, domain := range options.Domains { 40 | dotdomain := dns.Fqdn(domain) 41 | 42 | mxDomain := fmt.Sprintf("mail.%s", dotdomain) 43 | mxDomains[dotdomain] = mxDomain 44 | 45 | ns1Domain := fmt.Sprintf("ns1.%s", dotdomain) 46 | ns2Domain := fmt.Sprintf("ns2.%s", dotdomain) 47 | nsDomains[dotdomain] = []string{ns1Domain, ns2Domain} 48 | } 49 | 50 | server := &DNSServer{ 51 | options: options, 52 | ipAddress: net.ParseIP(options.IPAddress), 53 | mxDomains: mxDomains, 54 | nsDomains: nsDomains, 55 | timeToLive: 3600, 56 | customRecords: newCustomDNSRecordsServer(options.CustomRecords), 57 | } 58 | server.server = &dns.Server{ 59 | Addr: options.ListenIP + fmt.Sprintf(":%d", options.DnsPort), 60 | Net: network, 61 | Handler: server, 62 | } 63 | return server 64 | } 65 | 66 | // ListenAndServe listens on dns ports for the server. 67 | func (h *DNSServer) ListenAndServe(dnsAlive chan bool) { 68 | dnsAlive <- true 69 | if err := h.server.ListenAndServe(); err != nil { 70 | gologger.Error().Msgf("Could not listen for %s DNS on %s (%s)\n", strings.ToUpper(h.server.Net), h.server.Addr, err) 71 | dnsAlive <- false 72 | } 73 | } 74 | 75 | // ServeDNS is the default handler for DNS queries. 76 | func (h *DNSServer) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { 77 | atomic.AddUint64(&h.options.Stats.Dns, 1) 78 | 79 | m := new(dns.Msg) 80 | m.SetReply(r) 81 | m.Authoritative = true 82 | 83 | // bail early for no queries. 84 | if len(r.Question) == 0 { 85 | return 86 | } 87 | 88 | isDNSChallenge := false 89 | for _, question := range r.Question { 90 | domain := question.Name 91 | 92 | // Handle DNS server cases for ACME server 93 | if strings.HasPrefix(strings.ToLower(domain), acme.DNSChallengeString) { 94 | isDNSChallenge = true 95 | 96 | gologger.Debug().Msgf("Got acme dns request: \n%s\n", r.String()) 97 | 98 | switch question.Qtype { 99 | case dns.TypeSOA: 100 | h.handleSOA(domain, m) 101 | case dns.TypeTXT: 102 | err := h.handleACMETXTChallenge(domain, m) 103 | if err != nil { 104 | fmt.Printf("handleACMETXTChallenge for zone %s err: %+v\n", domain, err) 105 | return 106 | } 107 | case dns.TypeNS: 108 | h.handleNS(domain, m) 109 | case dns.TypeA, dns.TypeAAAA: 110 | h.handleACNAMEANY(domain, m) 111 | } 112 | 113 | gologger.Debug().Msgf("Got acme dns response: \n%s\n", m.String()) 114 | } else { 115 | switch question.Qtype { 116 | case dns.TypeA, dns.TypeAAAA, dns.TypeCNAME, dns.TypeANY: 117 | h.handleACNAMEANY(domain, m) 118 | case dns.TypeMX: 119 | h.handleMX(domain, m) 120 | case dns.TypeNS: 121 | h.handleNS(domain, m) 122 | case dns.TypeSOA: 123 | h.handleSOA(domain, m) 124 | case dns.TypeTXT: 125 | h.handleTXT(domain, m) 126 | } 127 | } 128 | } 129 | if !isDNSChallenge { 130 | // Write interaction for first question and dns request 131 | h.handleInteraction(r.Question[0].Name, w, r, m) 132 | } 133 | 134 | if err := w.WriteMsg(m); err != nil { 135 | gologger.Warning().Msgf("Could not write DNS response: \n%s\n %s\n", m.String(), err) 136 | } 137 | } 138 | 139 | // handleACMETXTChallenge handles solving of ACME TXT challenge with the given provider 140 | func (h *DNSServer) handleACMETXTChallenge(zone string, m *dns.Msg) error { 141 | records, err := h.options.ACMEStore.GetRecords(context.Background(), strings.ToLower(zone)) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | rrs := []dns.RR{} 147 | for _, record := range records { 148 | txtHdr := dns.RR_Header{Name: zone, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(record.TTL)} 149 | rrs = append(rrs, &dns.TXT{Hdr: txtHdr, Txt: []string{record.Value}}) 150 | } 151 | m.Answer = append(m.Answer, rrs...) 152 | return nil 153 | } 154 | 155 | // handleACNAMEANY handles A, CNAME or ANY queries for DNS server 156 | func (h *DNSServer) handleACNAMEANY(zone string, m *dns.Msg) { 157 | nsHeader := dns.RR_Header{Name: zone, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: h.timeToLive} 158 | 159 | // If we have a custom record serve it, or default IP 160 | record := h.customRecords.checkCustomResponse(zone) 161 | switch { 162 | case record != "": 163 | h.resultFunction(nsHeader, zone, net.ParseIP(record), m) 164 | default: 165 | h.resultFunction(nsHeader, zone, h.ipAddress, m) 166 | } 167 | } 168 | 169 | func (h *DNSServer) resultFunction(nsHeader dns.RR_Header, zone string, ipAddress net.IP, m *dns.Msg) { 170 | m.Answer = append(m.Answer, &dns.A{Hdr: dns.RR_Header{Name: zone, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: h.timeToLive}, A: ipAddress}) 171 | dotDomains := []string{zone, dns.Fqdn(h.options.Domains[0])} 172 | for _, dotDomain := range dotDomains { 173 | if nsDomains, ok := h.nsDomains[dotDomain]; ok { 174 | for _, nsDomain := range nsDomains { 175 | m.Ns = append(m.Ns, &dns.NS{Hdr: nsHeader, Ns: nsDomain}) 176 | 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}) 177 | } 178 | return 179 | } 180 | } 181 | } 182 | 183 | func (h *DNSServer) handleMX(zone string, m *dns.Msg) { 184 | nsHdr := dns.RR_Header{Name: zone, Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: h.timeToLive} 185 | 186 | dotDomains := []string{zone, dns.Fqdn(h.options.Domains[0])} 187 | for _, dotDomain := range dotDomains { 188 | if mxdomain, ok := h.mxDomains[dotDomain]; ok { 189 | m.Answer = append(m.Answer, &dns.MX{Hdr: nsHdr, Mx: mxdomain, Preference: 1}) 190 | return 191 | } 192 | } 193 | } 194 | 195 | func (h *DNSServer) handleNS(zone string, m *dns.Msg) { 196 | nsHeader := dns.RR_Header{Name: zone, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: h.timeToLive} 197 | 198 | dotDomains := []string{zone, dns.Fqdn(h.options.Domains[0])} 199 | for _, dotDomain := range dotDomains { 200 | if nsDomains, ok := h.nsDomains[dotDomain]; ok { 201 | for _, nsDomain := range nsDomains { 202 | m.Answer = append(m.Answer, &dns.NS{Hdr: nsHeader, Ns: nsDomain}) 203 | } 204 | return 205 | } 206 | } 207 | } 208 | 209 | func (h *DNSServer) handleSOA(zone string, m *dns.Msg) { 210 | nsHdr := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Class: dns.ClassINET} 211 | dotDomains := []string{zone, dns.Fqdn(h.options.Domains[0])} 212 | for _, dotDomain := range dotDomains { 213 | if nsDomains, ok := h.nsDomains[dotDomain]; ok { 214 | for _, nsDomain := range nsDomains { 215 | m.Answer = append(m.Answer, &dns.SOA{Hdr: nsHdr, Ns: nsDomain, Mbox: acme.CertificateAuthority, Serial: 1, Expire: 60, Minttl: 60}) 216 | return 217 | } 218 | } 219 | } 220 | } 221 | 222 | func (h *DNSServer) handleTXT(zone string, m *dns.Msg) { 223 | m.Answer = append(m.Answer, &dns.TXT{Hdr: dns.RR_Header{Name: zone, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 0}, Txt: []string{h.TxtRecord}}) 224 | } 225 | 226 | func toQType(ttype uint16) (rtype string) { 227 | switch ttype { 228 | case dns.TypeA: 229 | rtype = "A" 230 | case dns.TypeNS: 231 | rtype = "NS" 232 | case dns.TypeCNAME: 233 | rtype = "CNAME" 234 | case dns.TypeSOA: 235 | rtype = "SOA" 236 | case dns.TypePTR: 237 | rtype = "PTR" 238 | case dns.TypeMX: 239 | rtype = "MX" 240 | case dns.TypeTXT: 241 | rtype = "TXT" 242 | case dns.TypeAAAA: 243 | rtype = "AAAA" 244 | } 245 | return 246 | } 247 | 248 | // handleInteraction handles an interaction for the DNS server 249 | func (h *DNSServer) handleInteraction(domain string, w dns.ResponseWriter, r *dns.Msg, m *dns.Msg) { 250 | var uniqueID, fullID string 251 | 252 | requestMsg := r.String() 253 | responseMsg := m.String() 254 | 255 | gologger.Debug().Msgf("New DNS request: %s\n", requestMsg) 256 | 257 | var foundDomain string 258 | for _, configuredDomain := range h.options.Domains { 259 | configuredDotDomain := dns.Fqdn(configuredDomain) 260 | if stringsutil.HasSuffixI(domain, configuredDotDomain) { 261 | foundDomain = configuredDomain 262 | break 263 | } 264 | } 265 | 266 | // if root-tld is enabled stores any interaction towards the main domain 267 | if h.options.RootTLD && foundDomain != "" { 268 | correlationID := foundDomain 269 | host, _, _ := net.SplitHostPort(w.RemoteAddr().String()) 270 | interaction := &Interaction{ 271 | Protocol: "dns", 272 | UniqueID: domain, 273 | FullId: domain, 274 | QType: toQType(r.Question[0].Qtype), 275 | RawRequest: requestMsg, 276 | RawResponse: responseMsg, 277 | RemoteAddress: host, 278 | Timestamp: time.Now(), 279 | } 280 | 281 | if nil != h.options.OnResult { 282 | h.options.OnResult(interaction) 283 | } 284 | 285 | buffer := &bytes.Buffer{} 286 | if err := jsoniter.NewEncoder(buffer).Encode(interaction); err != nil { 287 | gologger.Warning().Msgf("Could not encode root tld dns interaction: %s\n", err) 288 | } else { 289 | gologger.Debug().Msgf("Root TLD DNS Interaction: \n%s\n", buffer.String()) 290 | if err := h.options.Storage.AddInteractionWithId(correlationID, buffer.Bytes()); err != nil { 291 | gologger.Warning().Msgf("Could not store dns interaction: %s\n", err) 292 | } 293 | } 294 | } 295 | 296 | if foundDomain != "" { 297 | parts := strings.Split(domain, ".") 298 | for i, part := range parts { 299 | if h.options.isCorrelationID(part) { 300 | uniqueID = part 301 | fullID = part 302 | if i+1 <= len(parts) { 303 | fullID = strings.Join(parts[:i+1], ".") 304 | } 305 | } 306 | } 307 | } 308 | uniqueID = strings.ToLower(uniqueID) 309 | 310 | if uniqueID != "" { 311 | correlationID := uniqueID[:h.options.CorrelationIdLength] 312 | host, _, _ := net.SplitHostPort(w.RemoteAddr().String()) 313 | interaction := &Interaction{ 314 | Protocol: "dns", 315 | UniqueID: uniqueID, 316 | FullId: fullID, 317 | QType: toQType(r.Question[0].Qtype), 318 | RawRequest: requestMsg, 319 | RawResponse: responseMsg, 320 | RemoteAddress: host, 321 | Timestamp: time.Now(), 322 | } 323 | buffer := &bytes.Buffer{} 324 | if err := jsoniter.NewEncoder(buffer).Encode(interaction); err != nil { 325 | gologger.Warning().Msgf("Could not encode dns interaction: %s\n", err) 326 | } else { 327 | gologger.Debug().Msgf("DNS Interaction: \n%s\n", buffer.String()) 328 | if err := h.options.Storage.AddInteraction(correlationID, buffer.Bytes()); err != nil { 329 | gologger.Warning().Msgf("Could not store dns interaction: %s\n", err) 330 | } 331 | } 332 | } 333 | } 334 | 335 | // customDNSRecords is a server for custom dns records 336 | type customDNSRecords struct { 337 | records map[string]string 338 | } 339 | 340 | // defaultCustomRecords is the list of default custom DNS records 341 | var defaultCustomRecords = map[string]string{ 342 | "aws": "169.254.169.254", 343 | "alibaba": "100.100.100.200", 344 | "localhost": "127.0.0.1", 345 | "oracle": "192.0.0.192", 346 | } 347 | 348 | func newCustomDNSRecordsServer(input string) *customDNSRecords { 349 | server := &customDNSRecords{records: make(map[string]string)} 350 | for k, v := range defaultCustomRecords { 351 | server.records[k] = v 352 | } 353 | if input != "" { 354 | if err := server.readRecordsFromFile(input); err != nil { 355 | gologger.Error().Msgf("Could not read custom DNS records: %s", err) 356 | } 357 | } 358 | return server 359 | } 360 | 361 | func (c *customDNSRecords) readRecordsFromFile(input string) error { 362 | file, err := os.Open(input) 363 | if err != nil { 364 | return errors.Wrap(err, "could not open file") 365 | } 366 | defer file.Close() 367 | 368 | var data map[string]string 369 | if err := yaml.NewDecoder(file).Decode(&data); err != nil { 370 | return errors.Wrap(err, "could not decode file") 371 | } 372 | for k, v := range data { 373 | c.records[strings.ToLower(k)] = v 374 | } 375 | return nil 376 | } 377 | 378 | func (c *customDNSRecords) checkCustomResponse(zone string) string { 379 | parts := strings.SplitN(zone, ".", 2) 380 | if len(parts) != 2 { 381 | return "" 382 | } 383 | if value, ok := c.records[strings.ToLower(parts[0])]; ok { 384 | return value 385 | } 386 | return "" 387 | } 388 | -------------------------------------------------------------------------------- /cmd/interactsh-client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | "time" 12 | 13 | jsoniter "github.com/json-iterator/go" 14 | "github.com/projectdiscovery/goflags" 15 | "github.com/projectdiscovery/gologger" 16 | "github.com/projectdiscovery/gologger/levels" 17 | "github.com/projectdiscovery/interactsh/internal/runner" 18 | "github.com/projectdiscovery/interactsh/pkg/client" 19 | "github.com/projectdiscovery/interactsh/pkg/options" 20 | "github.com/projectdiscovery/interactsh/pkg/server" 21 | "github.com/projectdiscovery/interactsh/pkg/settings" 22 | fileutil "github.com/projectdiscovery/utils/file" 23 | folderutil "github.com/projectdiscovery/utils/folder" 24 | updateutils "github.com/projectdiscovery/utils/update" 25 | ) 26 | 27 | var ( 28 | healthcheck bool 29 | defaultConfigLocation = filepath.Join(folderutil.HomeDirOrDefault("."), ".config/interactsh-client/config.yaml") 30 | ) 31 | 32 | func main() { 33 | gologger.DefaultLogger.SetMaxLevel(levels.LevelVerbose) 34 | 35 | defaultOpts := client.DefaultOptions 36 | cliOptions := &options.CLIClientOptions{} 37 | 38 | flagSet := goflags.NewFlagSet() 39 | flagSet.SetDescription(`Interactsh client - Go client to generate interactsh payloads and display interaction data.`) 40 | 41 | flagSet.CreateGroup("input", "Input", 42 | flagSet.StringVarP(&cliOptions.ServerURL, "server", "s", defaultOpts.ServerURL, "interactsh server(s) to use"), 43 | ) 44 | 45 | flagSet.CreateGroup("config", "config", 46 | flagSet.StringVar(&cliOptions.Config, "config", defaultConfigLocation, "flag configuration file"), 47 | flagSet.IntVarP(&cliOptions.NumberOfPayloads, "number", "n", 1, "number of interactsh payload to generate"), 48 | flagSet.StringVarP(&cliOptions.Token, "token", "t", "", "authentication token to connect protected interactsh server"), 49 | flagSet.IntVarP(&cliOptions.PollInterval, "poll-interval", "pi", 5, "poll interval in seconds to pull interaction data"), 50 | flagSet.BoolVarP(&cliOptions.DisableHTTPFallback, "no-http-fallback", "nf", false, "disable http fallback registration"), 51 | flagSet.IntVarP(&cliOptions.CorrelationIdLength, "correlation-id-length", "cidl", settings.CorrelationIdLengthDefault, "length of the correlation id preamble"), 52 | flagSet.IntVarP(&cliOptions.CorrelationIdNonceLength, "correlation-id-nonce-length", "cidn", settings.CorrelationIdNonceLengthDefault, "length of the correlation id nonce"), 53 | flagSet.StringVarP(&cliOptions.SessionFile, "session-file", "sf", "", "store/read from session file"), 54 | flagSet.DurationVarP(&cliOptions.KeepAliveInterval, "keep-alive-interval", "kai", time.Minute, "keep alive interval"), 55 | ) 56 | 57 | flagSet.CreateGroup("filter", "Filter", 58 | flagSet.StringSliceVarP(&cliOptions.Match, "match", "m", nil, "match interaction based on the specified pattern", goflags.FileCommaSeparatedStringSliceOptions), 59 | flagSet.StringSliceVarP(&cliOptions.Filter, "filter", "f", nil, "filter interaction based on the specified pattern", goflags.FileCommaSeparatedStringSliceOptions), 60 | flagSet.BoolVar(&cliOptions.DNSOnly, "dns-only", false, "display only dns interaction in CLI output"), 61 | flagSet.BoolVar(&cliOptions.HTTPOnly, "http-only", false, "display only http interaction in CLI output"), 62 | flagSet.BoolVar(&cliOptions.SmtpOnly, "smtp-only", false, "display only smtp interactions in CLI output"), 63 | flagSet.BoolVar(&cliOptions.Asn, "asn", false, " include asn information of remote ip in json output"), 64 | ) 65 | 66 | flagSet.CreateGroup("update", "Update", 67 | flagSet.CallbackVarP(options.GetUpdateCallback("interactsh-client"), "update", "up", "update interactsh-client to latest version"), 68 | flagSet.BoolVarP(&cliOptions.DisableUpdateCheck, "disable-update-check", "duc", false, "disable automatic interactsh-client update check"), 69 | ) 70 | 71 | flagSet.CreateGroup("output", "Output", 72 | flagSet.StringVar(&cliOptions.Output, "o", "", "output file to write interaction data"), 73 | flagSet.BoolVar(&cliOptions.JSON, "json", false, "write output in JSONL(ines) format"), 74 | flagSet.BoolVarP(&cliOptions.StorePayload, "payload-store", "ps", false, "write generated interactsh payload to file"), 75 | flagSet.StringVarP(&cliOptions.StorePayloadFile, "payload-store-file", "psf", settings.StorePayloadFileDefault, "store generated interactsh payloads to given file"), 76 | 77 | flagSet.BoolVar(&cliOptions.Verbose, "v", false, "display verbose interaction"), 78 | ) 79 | 80 | flagSet.CreateGroup("debug", "Debug", 81 | flagSet.BoolVar(&cliOptions.Version, "version", false, "show version of the project"), 82 | flagSet.BoolVarP(&healthcheck, "hc", "health-check", false, "run diagnostic check up"), 83 | ) 84 | 85 | if err := flagSet.Parse(); err != nil { 86 | gologger.Fatal().Msgf("Could not parse options: %s\n", err) 87 | } 88 | 89 | options.ShowBanner() 90 | 91 | if healthcheck { 92 | cfgFilePath, _ := flagSet.GetConfigFilePath() 93 | gologger.Print().Msgf("%s\n", runner.DoHealthCheck(cfgFilePath)) 94 | os.Exit(0) 95 | } 96 | if cliOptions.Version { 97 | gologger.Info().Msgf("Current Version: %s\n", options.Version) 98 | os.Exit(0) 99 | } 100 | 101 | if !cliOptions.DisableUpdateCheck { 102 | latestVersion, err := updateutils.GetToolVersionCallback("interactsh-client", options.Version)() 103 | if err != nil { 104 | if cliOptions.Verbose { 105 | gologger.Error().Msgf("interactsh version check failed: %v", err.Error()) 106 | } 107 | } else { 108 | gologger.Info().Msgf("Current interactsh version %v %v", options.Version, updateutils.GetVersionDescription(options.Version, latestVersion)) 109 | } 110 | } 111 | 112 | if cliOptions.Config != defaultConfigLocation { 113 | if err := flagSet.MergeConfigFile(cliOptions.Config); err != nil { 114 | gologger.Fatal().Msgf("Could not read config: %s\n", err) 115 | } 116 | } 117 | 118 | var outputFile *os.File 119 | var err error 120 | if cliOptions.Output != "" { 121 | if outputFile, err = os.Create(cliOptions.Output); err != nil { 122 | gologger.Fatal().Msgf("Could not create output file: %s\n", err) 123 | } 124 | defer outputFile.Close() 125 | } 126 | 127 | var sessionInfo *options.SessionInfo 128 | if fileutil.FileExists(cliOptions.SessionFile) { 129 | // attempt to load session info - silently ignore on failure 130 | _ = fileutil.Unmarshal(fileutil.YAML, []byte(cliOptions.SessionFile), &sessionInfo) 131 | } 132 | 133 | client, err := client.New(&client.Options{ 134 | ServerURL: cliOptions.ServerURL, 135 | Token: cliOptions.Token, 136 | DisableHTTPFallback: cliOptions.DisableHTTPFallback, 137 | CorrelationIdLength: cliOptions.CorrelationIdLength, 138 | CorrelationIdNonceLength: cliOptions.CorrelationIdNonceLength, 139 | SessionInfo: sessionInfo, 140 | }) 141 | if err != nil { 142 | gologger.Fatal().Msgf("Could not create client: %s\n", err) 143 | } 144 | 145 | interactshURLs := generatePayloadURL(cliOptions.NumberOfPayloads, client) 146 | 147 | gologger.Info().Msgf("Listing %d payload for OOB Testing\n", cliOptions.NumberOfPayloads) 148 | for _, interactshURL := range interactshURLs { 149 | gologger.Info().Msgf("%s\n", interactshURL) 150 | } 151 | 152 | if cliOptions.StorePayload && cliOptions.StorePayloadFile != "" { 153 | if err := os.WriteFile(cliOptions.StorePayloadFile, []byte(strings.Join(interactshURLs, "\n")), 0644); err != nil { 154 | gologger.Fatal().Msgf("Could not write to payload output file: %s\n", err) 155 | } 156 | } 157 | 158 | // show all interactions 159 | noFilter := !cliOptions.DNSOnly && !cliOptions.HTTPOnly && !cliOptions.SmtpOnly 160 | 161 | var matcher *regexMatcher 162 | var filter *regexMatcher 163 | if len(cliOptions.Match) > 0 { 164 | if matcher, err = newRegexMatcher(cliOptions.Match); err != nil { 165 | gologger.Fatal().Msgf("Could not compile matchers: %s\n", err) 166 | } 167 | } 168 | if len(cliOptions.Filter) > 0 { 169 | if filter, err = newRegexMatcher(cliOptions.Filter); err != nil { 170 | gologger.Fatal().Msgf("Could not compile filter: %s\n", err) 171 | } 172 | } 173 | 174 | err = client.StartPolling(time.Duration(cliOptions.PollInterval)*time.Second, func(interaction *server.Interaction) { 175 | if matcher != nil && !matcher.match(interaction.FullId) { 176 | return 177 | } 178 | if filter != nil && filter.match(interaction.FullId) { 179 | return 180 | } 181 | 182 | if cliOptions.Asn { 183 | _ = client.TryGetAsnInfo(interaction) 184 | } 185 | 186 | if !cliOptions.JSON { 187 | builder := &bytes.Buffer{} 188 | 189 | switch interaction.Protocol { 190 | case "dns": 191 | if noFilter || cliOptions.DNSOnly { 192 | builder.WriteString(fmt.Sprintf("[%s] Received DNS interaction (%s) from %s at %s", interaction.FullId, interaction.QType, interaction.RemoteAddress, interaction.Timestamp.Format("2006-01-02 15:04:05"))) 193 | if cliOptions.Verbose { 194 | builder.WriteString(fmt.Sprintf("\n-----------\nDNS Request\n-----------\n\n%s\n\n------------\nDNS Response\n------------\n\n%s\n\n", interaction.RawRequest, interaction.RawResponse)) 195 | } 196 | writeOutput(outputFile, builder) 197 | } 198 | case "http": 199 | if noFilter || cliOptions.HTTPOnly { 200 | builder.WriteString(fmt.Sprintf("[%s] Received HTTP interaction from %s at %s", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp.Format("2006-01-02 15:04:05"))) 201 | if cliOptions.Verbose { 202 | builder.WriteString(fmt.Sprintf("\n------------\nHTTP Request\n------------\n\n%s\n\n-------------\nHTTP Response\n-------------\n\n%s\n\n", interaction.RawRequest, interaction.RawResponse)) 203 | } 204 | writeOutput(outputFile, builder) 205 | } 206 | case "smtp": 207 | if noFilter || cliOptions.SmtpOnly { 208 | builder.WriteString(fmt.Sprintf("[%s] Received SMTP interaction from %s at %s", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp.Format("2006-01-02 15:04:05"))) 209 | if cliOptions.Verbose { 210 | builder.WriteString(fmt.Sprintf("\n------------\nSMTP Interaction\n------------\n\n%s\n\n", interaction.RawRequest)) 211 | } 212 | writeOutput(outputFile, builder) 213 | } 214 | case "ftp": 215 | if noFilter { 216 | builder.WriteString(fmt.Sprintf("Received FTP interaction from %s at %s", interaction.RemoteAddress, interaction.Timestamp.Format("2006-01-02 15:04:05"))) 217 | if cliOptions.Verbose { 218 | builder.WriteString(fmt.Sprintf("\n------------\nFTP Interaction\n------------\n\n%s\n\n", interaction.RawRequest)) 219 | } 220 | writeOutput(outputFile, builder) 221 | } 222 | case "responder", "smb": 223 | if noFilter { 224 | builder.WriteString(fmt.Sprintf("Received Responder/Smb interaction at %s", interaction.Timestamp.Format("2006-01-02 15:04:05"))) 225 | if cliOptions.Verbose { 226 | builder.WriteString(fmt.Sprintf("\n------------\nResponder/SMB Interaction\n------------\n\n%s\n\n", interaction.RawRequest)) 227 | } 228 | writeOutput(outputFile, builder) 229 | } 230 | case "ldap": 231 | if noFilter { 232 | builder.WriteString(fmt.Sprintf("[%s] Received LDAP interaction from %s at %s", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp.Format("2006-01-02 15:04:05"))) 233 | if cliOptions.Verbose { 234 | builder.WriteString(fmt.Sprintf("\n------------\nLDAP Interaction\n------------\n\n%s\n\n", interaction.RawRequest)) 235 | } 236 | writeOutput(outputFile, builder) 237 | } 238 | } 239 | } else { 240 | b, err := jsoniter.Marshal(interaction) 241 | if err != nil { 242 | gologger.Error().Msgf("Could not marshal json output: %s\n", err) 243 | } else { 244 | os.Stdout.Write(b) 245 | os.Stdout.Write([]byte("\n")) 246 | } 247 | if outputFile != nil { 248 | _, _ = outputFile.Write(b) 249 | _, _ = outputFile.Write([]byte("\n")) 250 | } 251 | } 252 | }) 253 | if err != nil { 254 | gologger.Error().Msgf(err.Error()) 255 | } 256 | 257 | c := make(chan os.Signal, 1) 258 | signal.Notify(c, os.Interrupt) 259 | for range c { 260 | if cliOptions.SessionFile != "" { 261 | _ = client.SaveSessionTo(cliOptions.SessionFile) 262 | } 263 | _ = client.StopPolling() 264 | // whether the session is saved/loaded it shouldn't be destroyed { 265 | if cliOptions.SessionFile == "" { 266 | client.Close() 267 | } 268 | os.Exit(1) 269 | } 270 | } 271 | 272 | func generatePayloadURL(numberOfPayloads int, client *client.Client) []string { 273 | interactshURLs := make([]string, numberOfPayloads) 274 | for i := 0; i < numberOfPayloads; i++ { 275 | interactshURLs[i] = client.URL() 276 | } 277 | return interactshURLs 278 | } 279 | 280 | func writeOutput(outputFile *os.File, builder *bytes.Buffer) { 281 | if outputFile != nil { 282 | _, _ = outputFile.Write(builder.Bytes()) 283 | _, _ = outputFile.Write([]byte("\n")) 284 | } 285 | gologger.Silent().Msgf("%s", builder.String()) 286 | } 287 | 288 | type regexMatcher struct { 289 | items []*regexp.Regexp 290 | } 291 | 292 | func newRegexMatcher(items []string) (*regexMatcher, error) { 293 | matcher := ®exMatcher{} 294 | for _, item := range items { 295 | if compiled, err := regexp.Compile(item); err != nil { 296 | return nil, err 297 | } else { 298 | matcher.items = append(matcher.items, compiled) 299 | } 300 | } 301 | return matcher, nil 302 | } 303 | 304 | func (m *regexMatcher) match(item string) bool { 305 | for _, regex := range m.items { 306 | if regex.MatchString(item) { 307 | return true 308 | } 309 | } 310 | return false 311 | } 312 | -------------------------------------------------------------------------------- /pkg/server/ldap_server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "fmt" 7 | "strings" 8 | "sync/atomic" 9 | "time" 10 | 11 | ldap "github.com/Mzack9999/ldapserver" 12 | jsoniter "github.com/json-iterator/go" 13 | "github.com/projectdiscovery/gologger" 14 | stringsutil "github.com/projectdiscovery/utils/strings" 15 | ) 16 | 17 | // Most routes handlers are taken from the example at https://github.com/vjeantet/ldapserver/blob/master/examples/complex/main.go 18 | 19 | func init() { 20 | ldap.Logger = ldap.DiscardingLogger 21 | } 22 | 23 | // LDAPServer is a ldap server instance 24 | type LDAPServer struct { 25 | WithLogger bool 26 | options *Options 27 | server *ldap.Server 28 | tlsConfig *tls.Config 29 | } 30 | 31 | // NewLDAPServer returns a new LDAP server. 32 | func NewLDAPServer(options *Options, withLogger bool) (*LDAPServer, error) { 33 | ldapserver := &LDAPServer{options: options, WithLogger: withLogger} 34 | 35 | if withLogger { 36 | ldap.Logger = ldapserver 37 | } 38 | 39 | routes := ldap.NewRouteMux() 40 | routes.Bind(ldapserver.handleBind) 41 | routes.NotFound(ldapserver.handleNotFound) 42 | routes.Abandon(ldapserver.handleAbandon) 43 | routes.Compare(ldapserver.handleCompare) 44 | routes.Add(ldapserver.handleAdd) 45 | routes.Delete(ldapserver.handleDelete) 46 | routes.Modify(ldapserver.handleModify) 47 | routes.Extended(ldapserver.handleStartTLS).RequestName(ldap.NoticeOfStartTLS).Label("StartTLS") 48 | routes.Extended(ldapserver.handleWhoAmI).RequestName(ldap.NoticeOfWhoAmI).Label("Ext - WhoAmI") 49 | routes.Extended(ldapserver.handleExtended).Label("Ext - Generic") 50 | routes.Search(ldapserver.handleSearch) 51 | 52 | server := ldap.NewServer() 53 | err := server.Handle(routes) 54 | if err != nil { 55 | return nil, err 56 | } 57 | ldapserver.server = server 58 | 59 | return ldapserver, nil 60 | } 61 | 62 | // ListenAndServe listens on ldap ports for the server. 63 | func (ldapServer *LDAPServer) ListenAndServe(tlsConfig *tls.Config, ldapAlive chan bool) { 64 | ldapAlive <- true 65 | ldapServer.tlsConfig = tlsConfig 66 | if err := ldapServer.server.ListenAndServe(fmt.Sprintf("%s:%d", ldapServer.options.ListenIP, ldapServer.options.LdapPort)); err != nil { 67 | gologger.Error().Msgf("Could not serve ldap on port 10389: %s\n", err) 68 | ldapAlive <- false 69 | } 70 | } 71 | 72 | // handleBind is a handler for bind requests 73 | func (ldapServer *LDAPServer) handleBind(w ldap.ResponseWriter, m *ldap.Message) { 74 | atomic.AddUint64(&ldapServer.options.Stats.Ldap, 1) 75 | 76 | r := m.GetBindRequest() 77 | res := ldap.NewBindResponse(ldap.LDAPResultSuccess) 78 | var message strings.Builder 79 | message.WriteString("Type=Bind\n") 80 | message.WriteString(fmt.Sprintf("AuthenticationChoice=%s\n", r.AuthenticationChoice())) 81 | message.WriteString(fmt.Sprintf("User=%s\n", r.Name())) 82 | message.WriteString(fmt.Sprintf("Pass=%s\n", r.Authentication())) 83 | w.Write(res) 84 | 85 | if ldapServer.WithLogger { 86 | ldapServer.logInteraction(Interaction{ 87 | RemoteAddress: m.Client.Addr().String(), 88 | RawRequest: message.String(), 89 | }) 90 | } 91 | } 92 | 93 | // handleSearch is a handler for search requests 94 | func (ldapServer *LDAPServer) handleSearch(w ldap.ResponseWriter, m *ldap.Message) { 95 | atomic.AddUint64(&ldapServer.options.Stats.Ldap, 1) 96 | 97 | var uniqueID, fullID string 98 | 99 | host := m.Client.Addr().String() 100 | 101 | r := m.GetSearchRequest() 102 | 103 | var message strings.Builder 104 | message.WriteString("Type=Search\n") 105 | baseObject := r.BaseObject() 106 | message.WriteString(fmt.Sprintf("BaseDn=%s\n", baseObject)) 107 | message.WriteString(fmt.Sprintf("Filter=%s\n", r.Filter())) 108 | message.WriteString(fmt.Sprintf("FilterString=%s\n", r.FilterString())) 109 | message.WriteString(fmt.Sprintf("Attributes=%s\n", r.Attributes())) 110 | message.WriteString(fmt.Sprintf("TimeLimit=%d\n", r.TimeLimit().Int())) 111 | 112 | e := ldap.NewSearchResultEntry("cn=interactsh, " + string(baseObject)) 113 | e.AddAttribute("mail", "interact@s.h", "interact@s.h") 114 | e.AddAttribute("company", "aaa") 115 | e.AddAttribute("department", "bbbb") 116 | e.AddAttribute("l", "cccc") 117 | e.AddAttribute("mobile", "123456789") 118 | e.AddAttribute("telephoneNumber", "123456789") 119 | e.AddAttribute("cn", "interact") 120 | w.Write(e) 121 | res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultSuccess) 122 | w.Write(res) 123 | 124 | for _, part := range stringsutil.SplitAny(string(baseObject), "=,") { 125 | partChunks := strings.Split(part, ".") 126 | for i, partChunk := range partChunks { 127 | for scanChunk := range stringsutil.SlideWithLength(partChunk, ldapServer.options.GetIdLength()) { 128 | if ldapServer.options.isCorrelationID(scanChunk) { 129 | uniqueID = scanChunk 130 | fullID = partChunk 131 | if i+1 <= len(partChunks) { 132 | fullID = strings.Join(partChunks[:i+1], ".") 133 | } 134 | ldapServer.handleInteraction(uniqueID, fullID, message.String(), host) 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | func (ldapServer *LDAPServer) handleInteraction(uniqueID, fullID, reqString, host string) { 142 | if uniqueID != "" { 143 | correlationID := uniqueID[:ldapServer.options.CorrelationIdLength] 144 | interaction := &Interaction{ 145 | Protocol: "ldap", 146 | UniqueID: uniqueID, 147 | FullId: fullID, 148 | RawRequest: reqString, 149 | RemoteAddress: host, 150 | Timestamp: time.Now(), 151 | } 152 | buffer := &bytes.Buffer{} 153 | if err := jsoniter.NewEncoder(buffer).Encode(interaction); err != nil { 154 | gologger.Warning().Msgf("Could not encode ldap interaction: %s\n", err) 155 | } else { 156 | gologger.Debug().Msgf("LDAP Interaction: \n%s\n", buffer.String()) 157 | if err := ldapServer.options.Storage.AddInteraction(correlationID, buffer.Bytes()); err != nil { 158 | gologger.Warning().Msgf("Could not store ldap interaction: %s\n", err) 159 | } 160 | } 161 | 162 | } 163 | 164 | // still not the full interaction without correlation if requested 165 | if ldapServer.WithLogger { 166 | ldapServer.logInteraction(Interaction{ 167 | RemoteAddress: host, 168 | RawRequest: reqString, 169 | }) 170 | } 171 | } 172 | 173 | // handleAbandon is a handler for abandon requests 174 | func (ldapServer *LDAPServer) handleAbandon(w ldap.ResponseWriter, m *ldap.Message) { 175 | atomic.AddUint64(&ldapServer.options.Stats.Ldap, 1) 176 | 177 | r := m.GetAbandonRequest() 178 | var message strings.Builder 179 | message.WriteString("Type=Abandon\n") 180 | 181 | if requestToAbandon, ok := m.Client.GetMessageByID(int(r)); ok { 182 | requestToAbandon.Abandon() 183 | } 184 | 185 | if ldapServer.WithLogger { 186 | ldapServer.logInteraction(Interaction{ 187 | RemoteAddress: m.Client.Addr().String(), 188 | RawRequest: message.String(), 189 | }) 190 | } 191 | } 192 | 193 | // handleNotFound is a handler for not matched routes requests 194 | func (ldapServer *LDAPServer) handleNotFound(w ldap.ResponseWriter, m *ldap.Message) { 195 | atomic.AddUint64(&ldapServer.options.Stats.Ldap, 1) 196 | 197 | var message strings.Builder 198 | message.WriteString(fmt.Sprintf("Type=%s\n", m.String())) 199 | 200 | switch m.ProtocolOpType() { 201 | case ldap.ApplicationBindRequest: 202 | res := ldap.NewBindResponse(ldap.LDAPResultSuccess) 203 | res.SetDiagnosticMessage("Default binding behavior set to return Success") 204 | w.Write(res) 205 | default: 206 | res := ldap.NewResponse(ldap.LDAPResultUnwillingToPerform) 207 | res.SetDiagnosticMessage("Operation not implemented by server") 208 | w.Write(res) 209 | } 210 | 211 | if ldapServer.WithLogger { 212 | ldapServer.logInteraction(Interaction{ 213 | RemoteAddress: m.Client.Addr().String(), 214 | RawRequest: message.String(), 215 | }) 216 | } 217 | } 218 | 219 | // handleCompare is a handler for compare requests 220 | func (ldapServer *LDAPServer) handleCompare(w ldap.ResponseWriter, m *ldap.Message) { 221 | atomic.AddUint64(&ldapServer.options.Stats.Ldap, 1) 222 | 223 | r := m.GetCompareRequest() 224 | var message strings.Builder 225 | message.WriteString("Type=Compare\n") 226 | message.WriteString(fmt.Sprintf("Attribute name to compare=%s\n", r.Ava().AttributeDesc())) 227 | message.WriteString(fmt.Sprintf("Attribute value expected=%s\n", r.Ava().AssertionValue())) 228 | 229 | res := ldap.NewCompareResponse(ldap.LDAPResultCompareTrue) 230 | w.Write(res) 231 | 232 | if ldapServer.WithLogger { 233 | ldapServer.logInteraction(Interaction{ 234 | RemoteAddress: m.Client.Addr().String(), 235 | RawRequest: message.String(), 236 | }) 237 | } 238 | } 239 | 240 | // handleCompare is a handler for compare requests 241 | func (ldapServer *LDAPServer) handleAdd(w ldap.ResponseWriter, m *ldap.Message) { 242 | atomic.AddUint64(&ldapServer.options.Stats.Ldap, 1) 243 | 244 | r := m.GetAddRequest() 245 | var message strings.Builder 246 | message.WriteString("Type=Add\n") 247 | message.WriteString(fmt.Sprintf("Entity=%s\n", r.Entry())) 248 | for _, attribute := range r.Attributes() { 249 | for _, attributeValue := range attribute.Vals() { 250 | message.WriteString(fmt.Sprintf("Attribute Name=%s Attribute Value=%s\n", attribute.Type_(), attributeValue)) 251 | } 252 | } 253 | 254 | res := ldap.NewAddResponse(ldap.LDAPResultSuccess) 255 | w.Write(res) 256 | 257 | if ldapServer.WithLogger { 258 | ldapServer.logInteraction(Interaction{ 259 | RemoteAddress: m.Client.Addr().String(), 260 | RawRequest: message.String(), 261 | }) 262 | } 263 | } 264 | 265 | // handleDelete is a handler for delete requests 266 | func (ldapServer *LDAPServer) handleDelete(w ldap.ResponseWriter, m *ldap.Message) { 267 | atomic.AddUint64(&ldapServer.options.Stats.Ldap, 1) 268 | 269 | r := m.GetCompareRequest() 270 | var message strings.Builder 271 | message.WriteString("Type=Delete\n") 272 | message.WriteString(fmt.Sprintf("Entity=%s\n", r.Entry())) 273 | 274 | res := ldap.NewDeleteResponse(ldap.LDAPResultSuccess) 275 | w.Write(res) 276 | 277 | if ldapServer.WithLogger { 278 | ldapServer.logInteraction(Interaction{ 279 | RemoteAddress: m.Client.Addr().String(), 280 | RawRequest: message.String(), 281 | }) 282 | } 283 | } 284 | 285 | // handleModify is a handler for delete requests 286 | func (ldapServer *LDAPServer) handleModify(w ldap.ResponseWriter, m *ldap.Message) { 287 | atomic.AddUint64(&ldapServer.options.Stats.Ldap, 1) 288 | 289 | r := m.GetModifyRequest() 290 | var message strings.Builder 291 | message.WriteString("Type=Modify\n") 292 | message.WriteString(fmt.Sprintf("Entity=%s\n", r.Object())) 293 | 294 | for _, change := range r.Changes() { 295 | modification := change.Modification() 296 | var operationString string 297 | switch change.Operation() { 298 | case ldap.ModifyRequestChangeOperationAdd: 299 | operationString = "Add" 300 | case ldap.ModifyRequestChangeOperationDelete: 301 | operationString = "Delete" 302 | case ldap.ModifyRequestChangeOperationReplace: 303 | operationString = "Replace" 304 | } 305 | 306 | var vals []string 307 | for _, attributeValue := range modification.Vals() { 308 | vals = append(vals, fmt.Sprint(attributeValue)) 309 | } 310 | message.WriteString(fmt.Sprintf("Operation=%s Attribute=%s Values=[%s]\n", operationString, modification.Type_(), strings.Join(vals, " - "))) 311 | } 312 | 313 | res := ldap.NewModifyResponse(ldap.LDAPResultSuccess) 314 | w.Write(res) 315 | 316 | if ldapServer.WithLogger { 317 | ldapServer.logInteraction(Interaction{ 318 | RemoteAddress: m.Client.Addr().String(), 319 | RawRequest: message.String(), 320 | }) 321 | } 322 | } 323 | 324 | // handleStartTLS is a handler for startTLS requests 325 | func (ldapServer *LDAPServer) handleStartTLS(w ldap.ResponseWriter, m *ldap.Message) { 326 | atomic.AddUint64(&ldapServer.options.Stats.Ldap, 1) 327 | 328 | var message strings.Builder 329 | message.WriteString("Type=StartTLS\n") 330 | 331 | tlsconfig, _ := ldapServer.getTLSconfig() 332 | tlsConn := tls.Server(m.Client.GetConn(), tlsconfig) 333 | res := ldap.NewExtendedResponse(ldap.LDAPResultSuccess) 334 | res.SetResponseName(ldap.NoticeOfStartTLS) 335 | w.Write(res) 336 | 337 | if err := tlsConn.Handshake(); err != nil { 338 | message.WriteString(fmt.Sprintf("Result=StartTLS Handshake error %s\n", err.Error())) 339 | res.SetDiagnosticMessage(fmt.Sprintf("StartTLS Handshake error : \"%s\"", err.Error())) 340 | res.SetResultCode(ldap.LDAPResultOperationsError) 341 | w.Write(res) 342 | return 343 | } 344 | m.Client.SetConn(tlsConn) 345 | message.WriteString("Result=StartTLS OK\n") 346 | 347 | if ldapServer.WithLogger { 348 | ldapServer.logInteraction(Interaction{ 349 | RemoteAddress: m.Client.Addr().String(), 350 | RawRequest: message.String(), 351 | }) 352 | } 353 | } 354 | 355 | // handleWhoAmI is a handler for whoami requests 356 | func (ldapServer *LDAPServer) handleWhoAmI(w ldap.ResponseWriter, m *ldap.Message) { 357 | atomic.AddUint64(&ldapServer.options.Stats.Ldap, 1) 358 | 359 | var message strings.Builder 360 | message.WriteString("Type=WhoAmI\n") 361 | 362 | res := ldap.NewExtendedResponse(ldap.LDAPResultSuccess) 363 | w.Write(res) 364 | 365 | if ldapServer.WithLogger { 366 | ldapServer.logInteraction(Interaction{ 367 | RemoteAddress: m.Client.Addr().String(), 368 | RawRequest: message.String(), 369 | }) 370 | } 371 | } 372 | 373 | // handleExtended is a handler for generic extended requests 374 | func (ldapServer *LDAPServer) handleExtended(w ldap.ResponseWriter, m *ldap.Message) { 375 | atomic.AddUint64(&ldapServer.options.Stats.Ldap, 1) 376 | 377 | r := m.GetExtendedRequest() 378 | 379 | var message strings.Builder 380 | message.WriteString("Type=Extended\n") 381 | message.WriteString(fmt.Sprintf("Name=%s\n", r.RequestName())) 382 | message.WriteString(fmt.Sprintf("Value=%s\n", r.RequestValue())) 383 | 384 | res := ldap.NewExtendedResponse(ldap.LDAPResultSuccess) 385 | w.Write(res) 386 | 387 | if ldapServer.WithLogger { 388 | ldapServer.logInteraction(Interaction{ 389 | RemoteAddress: m.Client.Addr().String(), 390 | RawRequest: message.String(), 391 | }) 392 | } 393 | } 394 | 395 | func (ldapServer *LDAPServer) Fatal(v ...interface{}) { 396 | //nolint 397 | ldapServer.handleLog("%v", v...) //nolint 398 | } 399 | func (ldapServer *LDAPServer) Fatalf(format string, v ...interface{}) { 400 | ldapServer.handleLog(format, v...) 401 | } 402 | func (ldapServer *LDAPServer) Fatalln(v ...interface{}) { 403 | ldapServer.handleLog("%v", v...) //nolint 404 | } 405 | func (ldapServer *LDAPServer) Panic(v ...interface{}) { 406 | ldapServer.handleLog("%v", v...) //nolint 407 | } 408 | func (ldapServer *LDAPServer) Panicf(format string, v ...interface{}) { 409 | ldapServer.handleLog(format, v...) 410 | } 411 | func (ldapServer *LDAPServer) Panicln(v ...interface{}) { 412 | ldapServer.handleLog("%v", v...) //nolint 413 | } 414 | func (ldapServer *LDAPServer) Print(v ...interface{}) { 415 | ldapServer.handleLog("%v", v...) //nolint 416 | } 417 | func (ldapServer *LDAPServer) Printf(format string, v ...interface{}) { 418 | ldapServer.handleLog(format, v...) 419 | } 420 | func (ldapServer *LDAPServer) Println(v ...interface{}) { 421 | ldapServer.handleLog("%v", v...) //nolint 422 | } 423 | 424 | func (ldapServer *LDAPServer) handleLog(f string, v ...interface{}) { 425 | // just discard logs if logger is disabled 426 | if !ldapServer.WithLogger { 427 | return 428 | } 429 | 430 | var data strings.Builder 431 | if f != "" { 432 | data.WriteString(fmt.Sprintf(f, v...)) 433 | } else { 434 | for _, vv := range v { 435 | data.WriteString(fmt.Sprint(vv)) 436 | } 437 | } 438 | 439 | // Correlation id doesn't apply here, we skip encryption 440 | ldapServer.logInteraction(Interaction{RawRequest: data.String()}) 441 | } 442 | 443 | func (ldapServer *LDAPServer) logInteraction(interaction Interaction) { 444 | // Correlation id doesn't apply here, we skip encryption 445 | interaction.Protocol = "ldap" 446 | interaction.Timestamp = time.Now() 447 | buffer := &bytes.Buffer{} 448 | if err := jsoniter.NewEncoder(buffer).Encode(interaction); err != nil { 449 | gologger.Warning().Msgf("Could not encode ldap interaction: %s\n", err) 450 | } else { 451 | gologger.Debug().Msgf("LDAP Interaction: \n%s\n", buffer.String()) 452 | if err := ldapServer.options.Storage.AddInteractionWithId(ldapServer.options.Token, buffer.Bytes()); err != nil { 453 | gologger.Warning().Msgf("Could not store ldap interaction: %s\n", err) 454 | } 455 | } 456 | } 457 | 458 | func (ldapServer *LDAPServer) Close() error { 459 | return ldapServer.server.Listener.Close() 460 | } 461 | 462 | // localhostCert is a PEM-encoded TLS cert with SAN DNS names 463 | // "127.0.0.1" and "[::1]", expiring at the last second of 2049 (the end 464 | // of ASN.1 time). 465 | var localhostCert = []byte(`-----BEGIN CERTIFICATE----- 466 | MIIBOTCB5qADAgECAgEAMAsGCSqGSIb3DQEBBTAAMB4XDTcwMDEwMTAwMDAwMFoX 467 | DTQ5MTIzMTIzNTk1OVowADBaMAsGCSqGSIb3DQEBAQNLADBIAkEAsuA5mAFMj6Q7 468 | qoBzcvKzIq4kzuT5epSp2AkcQfyBHm7K13Ws7u+0b5Vb9gqTf5cAiIKcrtrXVqkL 469 | 8i1UQF6AzwIDAQABo08wTTAOBgNVHQ8BAf8EBAMCACQwDQYDVR0OBAYEBAECAwQw 470 | DwYDVR0jBAgwBoAEAQIDBDAbBgNVHREEFDASggkxMjcuMC4wLjGCBVs6OjFdMAsG 471 | CSqGSIb3DQEBBQNBAJH30zjLWRztrWpOCgJL8RQWLaKzhK79pVhAx6q/3NrF16C7 472 | +l1BRZstTwIGdoGId8BRpErK1TXkniFb95ZMynM= 473 | -----END CERTIFICATE----- 474 | `) 475 | 476 | // localhostKey is the private key for localhostCert. 477 | var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- 478 | MIIBPQIBAAJBALLgOZgBTI+kO6qAc3LysyKuJM7k+XqUqdgJHEH8gR5uytd1rO7v 479 | tG+VW/YKk3+XAIiCnK7a11apC/ItVEBegM8CAwEAAQJBAI5sxq7naeR9ahyqRkJi 480 | SIv2iMxLuPEHaezf5CYOPWjSjBPyVhyRevkhtqEjF/WkgL7C2nWpYHsUcBDBQVF0 481 | 3KECIQDtEGB2ulnkZAahl3WuJziXGLB+p8Wgx7wzSM6bHu1c6QIhAMEp++CaS+SJ 482 | /TrU0zwY/fW4SvQeb49BPZUF3oqR8Xz3AiEA1rAJHBzBgdOQKdE3ksMUPcnvNJSN 483 | poCcELmz2clVXtkCIQCLytuLV38XHToTipR4yMl6O+6arzAjZ56uq7m7ZRV0TwIh 484 | AM65XAOw8Dsg9Kq78aYXiOEDc5DL0sbFUu/SlmRcCg93 485 | -----END RSA PRIVATE KEY----- 486 | `) 487 | 488 | // getTLSconfig returns a tls configuration used to build a TLSlistener for TLS or StartTLS 489 | func (ldapServer *LDAPServer) getTLSconfig() (*tls.Config, error) { 490 | if ldapServer.tlsConfig == nil { 491 | cert, err := tls.X509KeyPair(localhostCert, localhostKey) 492 | if err != nil { 493 | return &tls.Config{InsecureSkipVerify: true}, err 494 | } 495 | // SSL3.0 support is fine as we might be interacting with jurassic java 496 | return &tls.Config{ 497 | MinVersion: tls.VersionSSL30, //nolint 498 | MaxVersion: tls.VersionTLS12, 499 | Certificates: []tls.Certificate{cert}, 500 | ServerName: "127.0.0.1", 501 | }, nil 502 | } 503 | 504 | return ldapServer.tlsConfig, nil 505 | } 506 | -------------------------------------------------------------------------------- /pkg/server/http_server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "fmt" 7 | "log" 8 | "net" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/http/httputil" 12 | "os" 13 | "path/filepath" 14 | "strconv" 15 | "strings" 16 | "sync/atomic" 17 | "time" 18 | 19 | jsoniter "github.com/json-iterator/go" 20 | "github.com/projectdiscovery/gologger" 21 | stringsutil "github.com/projectdiscovery/utils/strings" 22 | ) 23 | 24 | // HTTPServer is a http server instance that listens both 25 | // TLS and Non-TLS based servers. 26 | type HTTPServer struct { 27 | options *Options 28 | tlsserver http.Server 29 | nontlsserver http.Server 30 | customBanner string 31 | staticHandler http.Handler 32 | } 33 | 34 | type noopLogger struct { 35 | } 36 | 37 | func (l *noopLogger) Write(p []byte) (n int, err error) { 38 | return 0, nil 39 | } 40 | 41 | // disableDirectoryListing disables directory listing on http.FileServer 42 | func disableDirectoryListing(next http.Handler) http.Handler { 43 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 44 | if strings.HasSuffix(r.URL.Path, "/") || r.URL.Path == "" { 45 | http.NotFound(w, r) 46 | return 47 | } 48 | next.ServeHTTP(w, r) 49 | }) 50 | } 51 | 52 | // NewHTTPServer returns a new TLS & Non-TLS HTTP server. 53 | func NewHTTPServer(options *Options) (*HTTPServer, error) { 54 | server := &HTTPServer{options: options} 55 | 56 | // If a static directory is specified, also serve it. 57 | if options.HTTPDirectory != "" { 58 | abs, _ := filepath.Abs(options.HTTPDirectory) 59 | gologger.Info().Msgf("Loading directory (%s) to serve from : %s/s/", abs, strings.Join(options.Domains, ",")) 60 | server.staticHandler = http.StripPrefix("/s/", disableDirectoryListing(http.FileServer(http.Dir(options.HTTPDirectory)))) 61 | } 62 | // If custom index, read the custom index file and serve it. 63 | // Supports {DOMAIN} placeholders. 64 | if options.HTTPIndex != "" { 65 | abs, _ := filepath.Abs(options.HTTPDirectory) 66 | gologger.Info().Msgf("Using custom server index: %s", abs) 67 | if data, err := os.ReadFile(options.HTTPIndex); err == nil { 68 | server.customBanner = string(data) 69 | } 70 | } 71 | router := &http.ServeMux{} 72 | router.Handle("/", server.logger(http.HandlerFunc(server.defaultHandler))) 73 | router.Handle("/register", server.corsMiddleware(server.authMiddleware(http.HandlerFunc(server.registerHandler)))) 74 | router.Handle("/deregister", server.corsMiddleware(server.authMiddleware(http.HandlerFunc(server.deregisterHandler)))) 75 | router.Handle("/poll", server.corsMiddleware(server.authMiddleware(http.HandlerFunc(server.pollHandler)))) 76 | if server.options.EnableMetrics { 77 | router.Handle("/metrics", server.corsMiddleware(server.authMiddleware(http.HandlerFunc(server.metricsHandler)))) 78 | } 79 | server.tlsserver = http.Server{Addr: options.ListenIP + fmt.Sprintf(":%d", options.HttpsPort), Handler: router, ErrorLog: log.New(&noopLogger{}, "", 0)} 80 | server.nontlsserver = http.Server{Addr: options.ListenIP + fmt.Sprintf(":%d", options.HttpPort), Handler: router, ErrorLog: log.New(&noopLogger{}, "", 0)} 81 | return server, nil 82 | } 83 | 84 | // ListenAndServe listens on http and/or https ports for the server. 85 | func (h *HTTPServer) ListenAndServe(tlsConfig *tls.Config, httpAlive, httpsAlive chan bool) { 86 | go func() { 87 | if tlsConfig == nil { 88 | return 89 | } 90 | h.tlsserver.TLSConfig = tlsConfig 91 | 92 | httpsAlive <- true 93 | if err := h.tlsserver.ListenAndServeTLS("", ""); err != nil { 94 | gologger.Error().Msgf("Could not serve http on tls: %s\n", err) 95 | httpsAlive <- false 96 | } 97 | }() 98 | 99 | httpAlive <- true 100 | if err := h.nontlsserver.ListenAndServe(); err != nil { 101 | httpAlive <- false 102 | gologger.Error().Msgf("Could not serve http: %s\n", err) 103 | } 104 | } 105 | 106 | func (h *HTTPServer) logger(handler http.Handler) http.HandlerFunc { 107 | return func(w http.ResponseWriter, r *http.Request) { 108 | req, _ := httputil.DumpRequest(r, true) 109 | reqString := string(req) 110 | 111 | gologger.Debug().Msgf("New HTTP request: \n\n%s\n", reqString) 112 | rec := httptest.NewRecorder() 113 | handler.ServeHTTP(rec, r) 114 | 115 | resp, _ := httputil.DumpResponse(rec.Result(), true) 116 | respString := string(resp) 117 | 118 | for k, v := range rec.Header() { 119 | w.Header()[k] = v 120 | } 121 | data := rec.Body.Bytes() 122 | 123 | w.WriteHeader(rec.Result().StatusCode) 124 | _, _ = w.Write(data) 125 | 126 | var host string 127 | // Check if the client's ip should be taken from a custom header (eg reverse proxy) 128 | if originIP := r.Header.Get(h.options.OriginIPHeader); originIP != "" { 129 | host = originIP 130 | } else { 131 | host, _, _ = net.SplitHostPort(r.RemoteAddr) 132 | } 133 | 134 | // if root-tld is enabled stores any interaction towards the main domain 135 | if h.options.RootTLD { 136 | for _, domain := range h.options.Domains { 137 | if h.options.RootTLD && stringsutil.HasSuffixI(r.Host, domain) { 138 | ID := domain 139 | host, _, _ := net.SplitHostPort(r.RemoteAddr) 140 | interaction := &Interaction{ 141 | Protocol: "http", 142 | UniqueID: r.Host, 143 | FullId: r.Host, 144 | RawRequest: reqString, 145 | RawResponse: respString, 146 | RemoteAddress: host, 147 | Timestamp: time.Now(), 148 | } 149 | buffer := &bytes.Buffer{} 150 | if err := jsoniter.NewEncoder(buffer).Encode(interaction); err != nil { 151 | gologger.Warning().Msgf("Could not encode root tld http interaction: %s\n", err) 152 | } else { 153 | gologger.Debug().Msgf("Root TLD HTTP Interaction: \n%s\n", buffer.String()) 154 | if err := h.options.Storage.AddInteractionWithId(ID, buffer.Bytes()); err != nil { 155 | gologger.Warning().Msgf("Could not store root tld http interaction: %s\n", err) 156 | } 157 | } 158 | } 159 | } 160 | } 161 | 162 | if h.options.ScanEverywhere { 163 | chunks := stringsutil.SplitAny(reqString, ".\n\t\"'") 164 | for _, chunk := range chunks { 165 | for part := range stringsutil.SlideWithLength(chunk, h.options.GetIdLength()) { 166 | normalizedPart := strings.ToLower(part) 167 | if h.options.isCorrelationID(normalizedPart) { 168 | h.handleInteraction(normalizedPart, part, reqString, respString, host) 169 | } 170 | } 171 | } 172 | } else { 173 | parts := strings.Split(r.Host, ".") 174 | for i, part := range parts { 175 | for partChunk := range stringsutil.SlideWithLength(part, h.options.GetIdLength()) { 176 | normalizedPartChunk := strings.ToLower(partChunk) 177 | if h.options.isCorrelationID(normalizedPartChunk) { 178 | fullID := part 179 | if i+1 <= len(parts) { 180 | fullID = strings.Join(parts[:i+1], ".") 181 | } 182 | h.handleInteraction(normalizedPartChunk, fullID, reqString, respString, host) 183 | } 184 | } 185 | } 186 | } 187 | } 188 | } 189 | 190 | func (h *HTTPServer) handleInteraction(uniqueID, fullID, reqString, respString, hostPort string) { 191 | correlationID := uniqueID[:h.options.CorrelationIdLength] 192 | 193 | interaction := &Interaction{ 194 | Protocol: "http", 195 | UniqueID: uniqueID, 196 | FullId: fullID, 197 | RawRequest: reqString, 198 | RawResponse: respString, 199 | RemoteAddress: hostPort, 200 | Timestamp: time.Now(), 201 | } 202 | buffer := &bytes.Buffer{} 203 | if err := jsoniter.NewEncoder(buffer).Encode(interaction); err != nil { 204 | gologger.Warning().Msgf("Could not encode http interaction: %s\n", err) 205 | } else { 206 | gologger.Debug().Msgf("HTTP Interaction: \n%s\n", buffer.String()) 207 | 208 | if err := h.options.Storage.AddInteraction(correlationID, buffer.Bytes()); err != nil { 209 | gologger.Warning().Msgf("Could not store http interaction: %s\n", err) 210 | } 211 | } 212 | } 213 | 214 | const banner = `