├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── issue-report.md ├── dependabot.yml ├── release.yml ├── stale.yml └── workflows │ ├── build-test.yml │ ├── codeql-analysis.yml │ ├── dep-auto-merge.yml │ ├── dockerhub-push.yml │ ├── functional-test.yml │ ├── lint-test.yml │ ├── release-binary.yml │ └── release-test.yml ├── .gitignore ├── .goreleaser ├── linux.yml ├── mac.yml └── windows.yml ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── THANKS.md ├── cmd ├── functional-test │ ├── main.go │ ├── run.sh │ ├── test-data │ │ └── request.txt │ └── testcases.txt ├── integration-test │ ├── cli.go │ ├── integration-test.go │ └── library.go └── naabu │ └── main.go ├── go.mod ├── go.sum ├── integration_tests └── run.sh ├── internal ├── pdcp │ ├── utils.go │ └── writer.go └── testutils │ └── integration.go ├── pkg ├── israce │ ├── norace.go │ └── race.go ├── port │ └── port.go ├── privileges │ ├── privileges.go │ ├── privileges_darwin.go │ ├── privileges_freebsd.go │ ├── privileges_linux.go │ ├── privileges_openbsd.go │ └── privileges_win.go ├── protocol │ └── protocol.go ├── result │ ├── confidence │ │ └── confidence.go │ ├── results.go │ └── results_test.go ├── routing │ ├── gateway_ip.go │ ├── router.go │ ├── router_darwin.go │ ├── router_freebsd.go │ ├── router_linux.go │ └── router_windows.go ├── runner │ ├── banners.go │ ├── banners_test.go │ ├── default.go │ ├── healthcheck.go │ ├── ips.go │ ├── ips_test.go │ ├── nmap.go │ ├── nmap_test.go │ ├── options.go │ ├── output.go │ ├── output_test.go │ ├── ports.go │ ├── ports_test.go │ ├── resume.go │ ├── runner.go │ ├── runner_test.go │ ├── targets.go │ ├── targets_test.go │ ├── util.go │ ├── util_test.go │ ├── validate.go │ └── validate_test.go ├── scan │ ├── arp_unix.go │ ├── arp_win.go │ ├── cdn.go │ ├── cdn_test.go │ ├── connect.go │ ├── connect_test.go │ ├── externalip.go │ ├── externalip_test.go │ ├── icmp.go │ ├── ndp.go │ ├── option.go │ ├── ping.go │ ├── ping_test.go │ ├── scan.go │ ├── scan_common.go │ ├── scan_unix.go │ ├── scan_win.go │ ├── tcpsequencer.go │ └── tcpsequencer_test.go └── utils │ └── limits │ ├── rate.go │ └── timeout.go └── static ├── naabu-logo.png └── naabu-run.png /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Ask an question / advise on using naabu 5 | url: https://github.com/projectdiscovery/naabu/discussions/categories/q-a 6 | about: Ask a question or request support for using naabu 7 | 8 | - name: Share idea / feature to discuss for naabu 9 | url: https://github.com/projectdiscovery/naabu/discussions/categories/ideas 10 | about: Share idea / feature to discuss for naabu 11 | 12 | - name: Connect with PD Team (Discord) 13 | url: https://discord.gg/projectdiscovery 14 | about: Connect with PD Team for direct communication -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request feature to implement in this project 4 | labels: 'Type: Enhancement' 5 | --- 6 | 7 | 13 | 14 | ### Please describe your feature request: 15 | 16 | 17 | ### Describe the use case of this feature: 18 | 19 | -------------------------------------------------------------------------------- /.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 | ### Naabu 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 | -------------------------------------------------------------------------------- /.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 go modules 10 | - package-ecosystem: "gomod" 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 | allow: 21 | - dependency-name: "github.com/projectdiscovery/*" 22 | 23 | # Maintain dependencies for docker 24 | - package-ecosystem: "docker" 25 | directory: "/" 26 | schedule: 27 | interval: "weekly" 28 | target-branch: "dev" 29 | commit-message: 30 | prefix: "chore" 31 | include: "scope" 32 | labels: 33 | - "Type: Maintenance" 34 | 35 | # Maintain dependencies for GitHub Actions 36 | - package-ecosystem: "github-actions" 37 | directory: "/" 38 | schedule: 39 | interval: "weekly" 40 | target-branch: "dev" 41 | commit-message: 42 | prefix: "chore" 43 | include: "scope" 44 | labels: 45 | - "Type: Maintenance" -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | categories: 6 | - title: 🎉 New Features 7 | labels: 8 | - "Type: Enhancement" 9 | - title: 🐞 Bugs Fixes 10 | labels: 11 | - "Type: Bug" 12 | - title: 🔨 Maintenance 13 | labels: 14 | - "Type: Maintenance" 15 | - title: Other Changes 16 | labels: 17 | - "*" -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 7 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 7 6 | 7 | # Issues with these labels will never be considered stale 8 | # exemptLabels: 9 | # - pinned 10 | # - security 11 | 12 | # Only issues or pull requests with all of these labels are check if stale. 13 | onlyLabels: 14 | - "Status: Abandoned" 15 | - "Type: Question" 16 | 17 | # Label to use when marking as stale 18 | staleLabel: stale 19 | 20 | # Comment to post when marking an issue as stale. Set to `false` to disable 21 | markComment: > 22 | This issue has been automatically marked as stale because it has not had 23 | recent activity. It will be closed if no further activity occurs. Thank you 24 | for your contributions. 25 | 26 | # Comment to post when closing a stale issue. Set to `false` to disable 27 | closeComment: false -------------------------------------------------------------------------------- /.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-linux: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: 1.21.x 21 | 22 | - name: Install libpcap-dev 23 | run: sudo apt install libpcap-dev 24 | 25 | - name: Install nmap 26 | run: sudo apt install nmap 27 | 28 | - name: Build 29 | run: go build . 30 | working-directory: cmd/naabu/ 31 | 32 | - name: Test 33 | run: go test -race ./... 34 | 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 - Standard User 42 | run: | 43 | go run -race . -host scanme.sh 44 | working-directory: cmd/naabu/ 45 | 46 | - name: Race Condition Tests - Root User 47 | run: | 48 | sudo go run -race . -host scanme.sh 49 | working-directory: cmd/naabu/ 50 | 51 | build-mac: 52 | runs-on: macos-latest 53 | steps: 54 | - name: Set up Go 55 | uses: actions/setup-go@v5 56 | with: 57 | go-version: 1.21.x 58 | 59 | - name: Check out code 60 | uses: actions/checkout@v4 61 | 62 | - name: Install libpcap-dev 63 | run: brew install libpcap 64 | 65 | - name: Install nmap 66 | run: brew install nmap 67 | 68 | - name: Build 69 | run: go build . 70 | working-directory: cmd/naabu/ 71 | 72 | - name: Test 73 | run: go test -race ./... 74 | 75 | - name: Integration Tests 76 | env: 77 | GH_ACTION: true 78 | run: bash run.sh 79 | working-directory: integration_tests/ 80 | 81 | - name: Race Condition Tests - Standard User 82 | run: | 83 | go run -race . -host scanme.sh 84 | working-directory: cmd/naabu/ 85 | 86 | - name: Race Condition Tests - Root User 87 | run: | 88 | sudo go run -race . -host scanme.sh 89 | working-directory: cmd/naabu/ 90 | 91 | build-windows: 92 | runs-on: windows-latest 93 | steps: 94 | - name: Set up Go 95 | uses: actions/setup-go@v5 96 | with: 97 | go-version: 1.21.x 98 | 99 | - name: Check out code 100 | uses: actions/checkout@v4 101 | 102 | - name: Build 103 | run: go build . 104 | working-directory: cmd/naabu/ 105 | 106 | - name: Test 107 | run: go test -race ./... 108 | 109 | - name: Race Condition Tests 110 | # Known issue: https://github.com/golang/go/issues/46099 111 | run: | 112 | # go run -race . -host scanme.sh 113 | # sudo go run -race . -host scanme.sh 114 | working-directory: cmd/naabu/ 115 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 🚨 CodeQL Analysis 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | paths: 7 | - '**.go' 8 | - '**.mod' 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | permissions: 14 | actions: read 15 | contents: read 16 | security-events: write 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | language: [ 'go' ] 22 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 23 | 24 | steps: 25 | - name: Install libpcap-dev 26 | run: sudo apt install libpcap-dev 27 | 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | 31 | # Initializes the CodeQL tools for scanning. 32 | - name: Initialize CodeQL 33 | uses: github/codeql-action/init@v3 34 | with: 35 | languages: ${{ matrix.language }} 36 | 37 | - name: Autobuild 38 | uses: github/codeql-action/autobuild@v3 39 | 40 | - name: Perform CodeQL Analysis 41 | uses: github/codeql-action/analyze@v3 -------------------------------------------------------------------------------- /.github/workflows/dep-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: 🤖 dep auto merge 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - dev 7 | workflow_dispatch: 8 | 9 | permissions: 10 | pull-requests: write 11 | issues: write 12 | repository-projects: write 13 | 14 | jobs: 15 | automerge: 16 | runs-on: ubuntu-latest 17 | if: github.actor == 'dependabot[bot]' 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | token: ${{ secrets.DEPENDABOT_PAT }} 22 | 23 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 24 | with: 25 | github-token: ${{ secrets.DEPENDABOT_PAT }} 26 | target: all -------------------------------------------------------------------------------- /.github/workflows/dockerhub-push.yml: -------------------------------------------------------------------------------- 1 | name: 🌥 Docker Push 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 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Get Github tag 18 | id: meta 19 | run: | 20 | curl --silent "https://api.github.com/repos/projectdiscovery/naabu/releases/latest" | jq -r .tag_name | xargs -I {} echo TAG={} >> $GITHUB_OUTPUT 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v3 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3 27 | 28 | - name: Login to DockerHub 29 | uses: docker/login-action@v3 30 | with: 31 | username: ${{ secrets.DOCKER_USERNAME }} 32 | password: ${{ secrets.DOCKER_TOKEN }} 33 | 34 | - name: Build and push 35 | uses: docker/build-push-action@v6 36 | with: 37 | context: . 38 | platforms: linux/amd64,linux/arm64,linux/arm 39 | push: true 40 | tags: projectdiscovery/naabu:latest,projectdiscovery/naabu:${{ steps.meta.outputs.TAG }} 41 | -------------------------------------------------------------------------------- /.github/workflows/functional-test.yml: -------------------------------------------------------------------------------- 1 | name: 🧪 Functional Test 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '**.go' 7 | - '**.mod' 8 | workflow_dispatch: 9 | 10 | jobs: 11 | functional: 12 | name: Functional Test 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest] # Todo: windows-latest, macOS-latest 17 | 18 | steps: 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: 1.21.x 23 | 24 | - name: Check out code 25 | uses: actions/checkout@v4 26 | 27 | - name: Install required packages (libpcap-dev, nmap, simplehttpserver) 28 | run: | 29 | sudo apt install libpcap-dev 30 | sudo apt install nmap 31 | go install -race github.com/projectdiscovery/simplehttpserver/cmd/simplehttpserver@latest 32 | 33 | - name: Run the simplehttpserver on 127.0.0.1:8000 34 | run: | 35 | export PATH=$PATH:$(go env GOPATH)/bin 36 | simplehttpserver -listen 127.0.0.1:8000 & 37 | 38 | - name: Functional Tests 39 | run: | 40 | chmod +x run.sh 41 | bash run.sh 42 | working-directory: cmd/functional-test 43 | -------------------------------------------------------------------------------- /.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@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: 1.21.x 22 | 23 | - name: Install Dependences 24 | run: sudo apt install libpcap-dev 25 | 26 | - name: Run golangci-lint 27 | uses: golangci/golangci-lint-action@v7.0.0 28 | with: 29 | version: latest 30 | args: --timeout 5m -------------------------------------------------------------------------------- /.github/workflows/release-binary.yml: -------------------------------------------------------------------------------- 1 | name: 🎉 Release Binary 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build-mac: 11 | runs-on: macos-latest 12 | steps: 13 | - name: Code checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: 1.21.x 21 | - name: Install Dependences 22 | run: brew install libpcap 23 | - name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v6 25 | with: 26 | version: latest 27 | args: release -f .goreleaser/mac.yml --clean 28 | workdir: / 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | build-linux: 33 | runs-on: ubuntu-latest-16-cores 34 | steps: 35 | - name: Code checkout 36 | uses: actions/checkout@v4 37 | with: 38 | fetch-depth: 0 39 | - name: Set up Go 40 | uses: actions/setup-go@v5 41 | with: 42 | go-version: 1.21.x 43 | - name: Install Dependences 44 | run: sudo apt install libpcap-dev 45 | 46 | - name: Run GoReleaser 47 | uses: goreleaser/goreleaser-action@v6 48 | with: 49 | version: latest 50 | args: release -f .goreleaser/linux.yml --clean 51 | workdir: / 52 | env: 53 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 54 | SLACK_WEBHOOK: "${{ secrets.RELEASE_SLACK_WEBHOOK }}" 55 | DISCORD_WEBHOOK_ID: "${{ secrets.DISCORD_WEBHOOK_ID }}" 56 | DISCORD_WEBHOOK_TOKEN: "${{ secrets.DISCORD_WEBHOOK_TOKEN }}" 57 | 58 | build-windows: 59 | runs-on: windows-latest-8-cores 60 | steps: 61 | - name: Code checkout 62 | uses: actions/checkout@v4 63 | with: 64 | fetch-depth: 0 65 | - name: Set up Go 66 | uses: actions/setup-go@v5 67 | with: 68 | go-version: 1.21.x 69 | - name: Run GoReleaser 70 | uses: goreleaser/goreleaser-action@v6 71 | with: 72 | version: latest 73 | args: release -f .goreleaser/windows.yml --clean 74 | workdir: / 75 | env: 76 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 77 | -------------------------------------------------------------------------------- /.github/workflows/release-test.yml: -------------------------------------------------------------------------------- 1 | name: 🔨 Release Test 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '**.go' 7 | - '**.mod' 8 | - '**.yml' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | release-test-mac: 13 | runs-on: macos-latest 14 | steps: 15 | - name: "Check out code" 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: 1.21.x 24 | 25 | - name: Install Dependences 26 | run: brew install libpcap 27 | 28 | - name: release test 29 | uses: goreleaser/goreleaser-action@v6 30 | with: 31 | args: "release --clean --snapshot -f .goreleaser/mac.yml" 32 | version: latest 33 | 34 | release-test-linux: 35 | runs-on: ubuntu-latest-16-cores 36 | steps: 37 | - name: "Check out code" 38 | uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 0 41 | 42 | - name: Set up Go 43 | uses: actions/setup-go@v5 44 | with: 45 | go-version: 1.21.x 46 | 47 | - name: Install Dependences 48 | run: sudo apt install libpcap-dev 49 | 50 | - name: release test 51 | uses: goreleaser/goreleaser-action@v6 52 | with: 53 | args: "release --clean --snapshot -f .goreleaser/linux.yml" 54 | version: latest 55 | 56 | release-test-windows: 57 | runs-on: windows-latest-8-cores 58 | steps: 59 | - name: "Check out code" 60 | uses: actions/checkout@v4 61 | with: 62 | fetch-depth: 0 63 | 64 | - name: Set up Go 65 | uses: actions/setup-go@v5 66 | with: 67 | go-version: 1.21.x 68 | 69 | - name: release test 70 | uses: goreleaser/goreleaser-action@v6 71 | with: 72 | args: "release --clean --snapshot -f .goreleaser/windows.yml" 73 | version: latest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cmd/naabu/naabu* 2 | vendor 3 | integration_tests/naabu 4 | integration_tests/integration-test 5 | cmd/functional-test/naabu_dev 6 | cmd/functional-test/functional-test 7 | cmd/functional-test/naabu 8 | cmd/functional-test/*.cfg 9 | .vscode 10 | dist -------------------------------------------------------------------------------- /.goreleaser/linux.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | before: 4 | hooks: 5 | - go mod tidy 6 | project_name: naabu 7 | builds: 8 | - id: naabu-linux 9 | ldflags: 10 | - -s -w 11 | binary: naabu 12 | env: 13 | - CGO_ENABLED=1 14 | main: ./cmd/naabu/main.go 15 | goos: 16 | - linux 17 | goarch: 18 | - amd64 19 | # - arm64 20 | 21 | archives: 22 | - format: zip 23 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ if eq .Os "darwin" }}macOS{{ else }}{{ .Os }}{{ end }}_{{ .Arch }}' 24 | 25 | checksum: 26 | name_template: "{{ .ProjectName }}-linux-checksums.txt" 27 | 28 | announce: 29 | slack: 30 | enabled: true 31 | channel: '#release' 32 | username: GoReleaser 33 | message_template: 'New Release: {{ .ProjectName }} {{.Tag}} is published! Check it out at {{ .ReleaseURL }}' 34 | 35 | discord: 36 | enabled: true 37 | message_template: '**New Release: {{ .ProjectName }} {{.Tag}}** is published! Check it out at {{ .ReleaseURL }}' -------------------------------------------------------------------------------- /.goreleaser/mac.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | before: 4 | hooks: 5 | - go mod tidy 6 | project_name: naabu 7 | builds: 8 | - id: naabu-darwin 9 | ldflags: 10 | - -s -w 11 | binary: naabu 12 | env: 13 | - CGO_ENABLED=1 14 | main: ./cmd/naabu/main.go 15 | goos: 16 | - darwin 17 | goarch: 18 | - amd64 19 | - arm64 20 | archives: 21 | - format: zip 22 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ if eq .Os "darwin" }}macOS{{ else }}{{ .Os }}{{ end }}_{{ .Arch }}' 23 | 24 | 25 | checksum: 26 | name_template: "{{ .ProjectName }}-mac-checksums.txt" 27 | -------------------------------------------------------------------------------- /.goreleaser/windows.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | before: 4 | hooks: 5 | - go mod tidy 6 | project_name: naabu 7 | builds: 8 | - id: naabu-windows 9 | ldflags: 10 | - -s -w 11 | binary: naabu 12 | # env: 13 | # - CGO_ENABLED=1 # necessary only with winpcap 14 | main: ./cmd/naabu/main.go 15 | goos: 16 | - windows 17 | goarch: 18 | - amd64 19 | - arm64 20 | - 386 21 | archives: 22 | - format: zip 23 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ if eq .Os "darwin" }}macOS{{ else }}{{ .Os }}{{ end }}_{{ .Arch }}' 24 | 25 | checksum: 26 | name_template: "{{ .ProjectName }}-windows-checksums.txt" 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build 2 | FROM golang:1.24.3-alpine AS build-env 3 | RUN apk add --no-cache build-base libpcap-dev 4 | WORKDIR /app 5 | COPY . /app 6 | RUN go mod download 7 | RUN go build ./cmd/naabu 8 | 9 | # Release 10 | FROM alpine:3.21.3 11 | RUN apk upgrade --no-cache \ 12 | && apk add --no-cache nmap libpcap-dev bind-tools ca-certificates nmap-scripts 13 | COPY --from=build-env /app/naabu /usr/local/bin/ 14 | ENTRYPOINT ["naabu"] -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Go parameters 2 | GOCMD=go 3 | GOBUILD=$(GOCMD) build 4 | GOMOD=$(GOCMD) mod 5 | GOTEST=$(GOCMD) test 6 | GOFLAGS := -v 7 | LDFLAGS := -s -w 8 | 9 | ifneq ($(shell go env GOOS),darwin) 10 | LDFLAGS := -extldflags "-static" 11 | endif 12 | 13 | all: build 14 | build: 15 | $(GOBUILD) $(GOFLAGS) -ldflags '$(LDFLAGS)' -o "naabu" cmd/naabu/main.go 16 | test: 17 | $(GOTEST) $(GOFLAGS) ./... 18 | tidy: 19 | $(GOMOD) tidy -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | naabu 3 |
4 |

5 | 6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 |

14 | 15 |

16 | Features • 17 | Installation • 18 | Usage • 19 | Running naabu • 20 | Config • 21 | NMAP integration • 22 | CDN/WAF Exclusion • 23 | Discord 24 |

25 | 26 | Naabu is a port scanning tool written in Go that allows you to enumerate valid ports for hosts in a fast and reliable manner. It is a really simple tool that does fast SYN/CONNECT/UDP scans on the host/list of hosts and lists 27 | all ports that return a reply. 28 | 29 | # Features 30 | 31 |

32 | naabu 33 |
34 |

35 | 36 | - Fast And Simple **SYN/CONNECT/UDP** probe based scanning 37 | - Optimized for ease of use and **lightweight** on resources 38 | - **DNS** Port scan 39 | - **Automatic IP Deduplication** for DNS port scan 40 | - **IPv4/IPv6** Port scan (**experimental**) 41 | - **Passive** Port enumeration using Shodan [Internetdb](https://internetdb.shodan.io) 42 | - **Host Discovery** scan (**experimental**) 43 | - **NMAP** integration for service discovery 44 | - Multiple input support - **STDIN/HOST/IP/CIDR/ASN** 45 | - Multiple output format support - **JSON/TXT/STDOUT** 46 | 47 | # Usage 48 | 49 | ```sh 50 | naabu -h 51 | ``` 52 | 53 | This will display help for the tool. Here are all the switches it supports. 54 | 55 | ```yaml 56 | Usage: 57 | naabu [flags] 58 | 59 | Flags: 60 | INPUT: 61 | -host string[] hosts to scan ports for (comma-separated) 62 | -list, -l string list of hosts to scan ports (file) 63 | -exclude-hosts, -eh string hosts to exclude from the scan (comma-separated) 64 | -exclude-file, -ef string list of hosts to exclude from scan (file) 65 | 66 | PORT: 67 | -port, -p string ports to scan (80,443, 100-200) 68 | -top-ports, -tp string top ports to scan (default 100) [full,100,1000] 69 | -exclude-ports, -ep string ports to exclude from scan (comma-separated) 70 | -ports-file, -pf string list of ports to scan (file) 71 | -port-threshold, -pts int port threshold to skip port scan for the host 72 | -exclude-cdn, -ec skip full port scans for CDN/WAF (only scan for port 80,443) 73 | -display-cdn, -cdn display cdn in use 74 | 75 | RATE-LIMIT: 76 | -c int general internal worker threads (default 25) 77 | -rate int packets to send per second (default 1000) 78 | 79 | UPDATE: 80 | -up, -update update naabu to latest version 81 | -duc, -disable-update-check disable automatic naabu update check 82 | 83 | OUTPUT: 84 | -o, -output string file to write output to (optional) 85 | -j, -json write output in JSON lines format 86 | -csv write output in csv format 87 | 88 | CONFIGURATION: 89 | -config string path to the naabu configuration file (default $HOME/.config/naabu/config.yaml) 90 | -scan-all-ips, -sa scan all the IP's associated with DNS record 91 | -ip-version, -iv string[] ip version to scan of hostname (4,6) - (default 4) (default ["4"]) 92 | -scan-type, -s string type of port scan (SYN/CONNECT) (default "c") 93 | -source-ip string source ip and port (x.x.x.x:yyy - might not work on OSX) 94 | -interface-list, -il list available interfaces and public ip 95 | -interface, -i string network Interface to use for port scan 96 | -nmap invoke nmap scan on targets (nmap must be installed) - Deprecated 97 | -nmap-cli string nmap command to run on found results (example: -nmap-cli 'nmap -sV') 98 | -r string list of custom resolver dns resolution (comma separated or from file) 99 | -proxy string socks5 proxy (ip[:port] / fqdn[:port] 100 | -proxy-auth string socks5 proxy authentication (username:password) 101 | -resume resume scan using resume.cfg 102 | -stream stream mode (disables resume, nmap, verify, retries, shuffling, etc) 103 | -passive display passive open ports using shodan internetdb api 104 | -irt, -input-read-timeout value timeout on input read (default 3m0s) 105 | -no-stdin Disable Stdin processing 106 | 107 | HOST-DISCOVERY: 108 | -sn, -host-discovery Perform Only Host Discovery 109 | -Pn, -skip-host-discovery Skip Host discovery (Deprecated: use -wn/-with-host-discovery instead) 110 | -wn, -with-host-discovery Enable Host discovery 111 | -ps, -probe-tcp-syn string[] TCP SYN Ping (host discovery needs to be enabled) 112 | -pa, -probe-tcp-ack string[] TCP ACK Ping (host discovery needs to be enabled) 113 | -pe, -probe-icmp-echo ICMP echo request Ping (host discovery needs to be enabled) 114 | -pp, -probe-icmp-timestamp ICMP timestamp request Ping (host discovery needs to be enabled) 115 | -pm, -probe-icmp-address-mask ICMP address mask request Ping (host discovery needs to be enabled) 116 | -arp, -arp-ping ARP ping (host discovery needs to be enabled) 117 | -nd, -nd-ping IPv6 Neighbor Discovery (host discovery needs to be enabled) 118 | -rev-ptr Reverse PTR lookup for input ips 119 | 120 | OPTIMIZATION: 121 | -retries int number of retries for the port scan (default 3) 122 | -timeout int millisecond to wait before timing out (default 1000) 123 | -warm-up-time int time in seconds between scan phases (default 2) 124 | -ping ping probes for verification of host 125 | -verify validate the ports again with TCP verification 126 | 127 | DEBUG: 128 | -health-check, -hc run diagnostic check up 129 | -debug display debugging information 130 | -verbose, -v display verbose output 131 | -no-color, -nc disable colors in CLI output 132 | -silent display only results in output 133 | -version display version of naabu 134 | -stats display stats of the running scan (deprecated) 135 | -si, -stats-interval int number of seconds to wait between showing a statistics update (deprecated) (default 5) 136 | -mp, -metrics-port int port to expose naabu metrics on (default 63636) 137 | 138 | CLOUD: 139 | -auth configure projectdiscovery cloud (pdcp) api key (default true) 140 | -ac, -auth-config string configure projectdiscovery cloud (pdcp) api key credential file 141 | -pd, -dashboard upload / view output in projectdiscovery cloud (pdcp) UI dashboard 142 | -tid, -team-id string upload asset results to given team id (optional) 143 | -aid, -asset-id string upload new assets to existing asset id (optional) 144 | -aname, -asset-name string assets group name to set (optional) 145 | -pdu, -dashboard-upload string upload naabu output file (jsonl) in projectdiscovery cloud (pdcp) UI dashboard 146 | ``` 147 | 148 | # Installation Instructions 149 | 150 | Download the ready to run [binary](https://github.com/projectdiscovery/naabu/releases/) / [docker](https://hub.docker.com/r/projectdiscovery/naabu) or install with GO 151 | 152 | ## Prerequisite 153 | 154 | > **Note**: before installing naabu, make sure to install `libpcap` library for packet capturing. 155 | 156 | To install libcap on **Linux**: `sudo apt install -y libpcap-dev`, on **Mac**: `brew install libpcap` 157 | 158 | 159 | ## Installing Naabu 160 | 161 | ```sh 162 | go install -v github.com/projectdiscovery/naabu/v2/cmd/naabu@latest 163 | ``` 164 | 165 | # Running Naabu 166 | 167 | To run the tool on a target, just use the following command. 168 | ```sh 169 | naabu -host hackerone.com 170 | ``` 171 | 172 | This will run the tool against hackerone.com. There are a number of configuration options that you can pass along with this command. The verbose switch `-v` can be used to display verbose information. 173 | 174 | ```console 175 | naabu -host hackerone.com 176 | 177 | __ 178 | ___ ___ ___ _/ / __ __ 179 | / _ \/ _ \/ _ \/ _ \/ // / 180 | /_//_/\_,_/\_,_/_.__/\_,_/ v2.0.3 181 | 182 | projectdiscovery.io 183 | 184 | [WRN] Use with caution. You are responsible for your actions 185 | [WRN] Developers assume no liability and are not responsible for any misuse or damage. 186 | [INF] Running SYN scan with root privileges 187 | [INF] Found 4 ports on host hackerone.com (104.16.100.52) 188 | 189 | hackerone.com:80 190 | hackerone.com:443 191 | hackerone.com:8443 192 | hackerone.com:8080 193 | ``` 194 | 195 | The ports to scan for on the host can be specified via `-p` parameter (udp ports must be expressed as `u:port`). It takes nmap format ports and runs enumeration on them. 196 | 197 | ```sh 198 | naabu -p 80,443,21-23,u:53 -host hackerone.com 199 | ``` 200 | 201 | By default, the Naabu checks for nmap's `Top 100` ports. It supports the following in-built port lists - 202 | 203 | | Flag | Description | 204 | |-------------------|--------------------------------------| 205 | | `-top-ports 100` | Scan for nmap top **100** port | 206 | | `-top-ports 1000` | Scan for nmap top **1000** port | 207 | | `-p - ` | Scan for full ports from **1-65535** | 208 | 209 | You can also specify specific ports which you would like to exclude from the scan. 210 | 211 | ```sh 212 | naabu -p - -exclude-ports 80,443 213 | ``` 214 | 215 | To run the naabu on a list of hosts, `-list` option can be used. 216 | 217 | ```sh 218 | naabu -list hosts.txt 219 | ``` 220 | To run the naabu on a ASN, AS input can be used. It takes the IP address available for given ASN and runs the enumeration on them. 221 | 222 | ```console 223 | echo AS14421 | naabu -p 80,443 224 | 225 | 216.101.17.249:80 226 | 216.101.17.249:443 227 | 216.101.17.248:443 228 | 216.101.17.252:443 229 | 216.101.17.251:80 230 | 216.101.17.251:443 231 | 216.101.17.250:443 232 | 216.101.17.250:80 233 | ``` 234 | You can also get output in json format using `-json` switch. This switch saves the output in the JSON lines format. 235 | 236 | ```console 237 | naabu -host 104.16.99.52 -json 238 | 239 | {"ip":"104.16.99.52","port":443} 240 | {"ip":"104.16.99.52","port":80} 241 | ``` 242 | 243 | The ports discovered can be piped to other tools too. For example, you can pipe the ports discovered by naabu to [httpx](https://github.com/projectdiscovery/httpx) which will then find running http servers on the host. 244 | 245 | ```console 246 | echo hackerone.com | naabu -silent | httpx -silent 247 | 248 | http://hackerone.com:8443 249 | http://hackerone.com:443 250 | http://hackerone.com:8080 251 | http://hackerone.com:80 252 | ``` 253 | 254 | The speed can be controlled by changing the value of `rate` flag that represent the number of packets per second. Increasing it while processing hosts may lead to increased false-positive rates. So it is recommended to keep it to a reasonable amount. 255 | 256 | # IPv4 and IPv6 257 | 258 | Naabu supports both IPv4 and IPv6. Both ranges can be piped together as input. If IPv6 is used, connectivity must be correctly configured, and the network interface must have an IPv6 address assigned (`inet6`) and a default gateway. 259 | 260 | ```console 261 | echo hackerone.com | dnsx -resp-only -a -aaaa -silent | naabu -p 80 -silent 262 | 263 | 104.16.99.52:80 264 | 104.16.100.52:80 265 | 2606:4700::6810:6434:80 266 | 2606:4700::6810:6334:80 267 | ``` 268 | 269 | The option `-ip-version 6` makes the tool use IPv6 addresses while resolving domain names. 270 | 271 | ```console 272 | echo hackerone.com | ./naabu -p 80 -ip-version 6 273 | 274 | __ 275 | ___ ___ ___ _/ / __ __ 276 | / _ \/ _ \/ _ \/ _ \/ // / 277 | /_//_/\_,_/\_,_/_.__/\_,_/ v2.0.8 278 | 279 | projectdiscovery.io 280 | 281 | Use with caution. You are responsible for your actions 282 | Developers assume no liability and are not responsible for any misuse or damage. 283 | [INF] Running CONNECT scan with non root privileges 284 | [INF] Found 1 ports on host hackerone.com (2606:4700::6810:6334) 285 | hackerone.com:80 286 | ``` 287 | 288 | To scan all the IPs of both version, `ip-version 4,6` can be used along with `-scan-all-ips` flag. 289 | 290 | ```console 291 | echo hackerone.com | ./naabu -iv 4,6 -sa -p 80 -silent 292 | 293 | [INF] Found 1 ports on host hackerone.com (104.16.100.52) 294 | hackerone.com:80 295 | [INF] Found 1 ports on host hackerone.com (104.16.99.52) 296 | hackerone.com:80 297 | [INF] Found 1 ports on host hackerone.com (2606:4700::6810:6334) 298 | hackerone.com:80 299 | [INF] Found 1 ports on host hackerone.com (2606:4700::6810:6434) 300 | hackerone.com:80 301 | ``` 302 | 303 | # Host Discovery 304 | 305 | Naabu optionally supports multiple options to perform host discovery. Host discovery is optional and can be enabled with the `-wn` flag. `-sn` flag instructs the tool to perform host discovery only. 306 | 307 | Available options to perform host discovery: 308 | 309 | - **ARP** ping (`-arp`) 310 | - TCP **SYN** ping (`-ps 80`) 311 | - TCP **ACK** ping (`-pa 443`) 312 | - ICMP **echo** ping (`-pe`) 313 | - ICMP **timestamp** ping (`-pp`) 314 | - ICMP **address mask** ping (`-pm`) 315 | - IPv6 **neighbor discovery** (`-nd`) 316 | 317 | # Configuration file 318 | 319 | Naabu supports config file as default located at `$HOME/.config/naabu/config.yaml`, It allows you to define any flag in the config file and set default values to include for all scans. 320 | 321 | 322 | # Nmap integration 323 | 324 | We have integrated nmap support for service discovery or any additional scans supported by nmap on the found results by Naabu, make sure you have `nmap` installed to use this feature. 325 | 326 | To use,`nmap-cli` flag can be used followed by nmap command, for example:- 327 | 328 | ```console 329 | echo hackerone.com | naabu -nmap-cli 'nmap -sV -oX nmap-output' 330 | __ 331 | ___ ___ ___ _/ / __ __ 332 | / _ \/ _ \/ _ \/ _ \/ // / 333 | /_//_/\_,_/\_,_/_.__/\_,_/ v2.0.0 334 | 335 | projectdiscovery.io 336 | 337 | [WRN] Use with caution. You are responsible for your actions 338 | [WRN] Developers assume no liability and are not responsible for any misuse or damage. 339 | [INF] Running TCP/ICMP/SYN scan with root privileges 340 | [INF] Found 4 ports on host hackerone.com (104.16.99.52) 341 | 342 | hackerone.com:443 343 | hackerone.com:80 344 | hackerone.com:8443 345 | hackerone.com:8080 346 | 347 | [INF] Running nmap command: nmap -sV -p 80,8443,8080,443 104.16.99.52 348 | 349 | Starting Nmap 7.01 ( https://nmap.org ) at 2020-09-23 05:02 UTC 350 | Nmap scan report for 104.16.99.52 351 | Host is up (0.0021s latency). 352 | PORT STATE SERVICE VERSION 353 | 80/tcp open http cloudflare 354 | 443/tcp open ssl/https cloudflare 355 | 8080/tcp open http-proxy cloudflare 356 | 8443/tcp open ssl/https-alt cloudflare 357 | ``` 358 | 359 | # CDN/WAF Exclusion 360 | 361 | Naabu also supports excluding CDN/WAF IPs being port scanned. If used, only `80` and `443` ports get scanned for those IPs. This feature can be enabled by using `exclude-cdn` flag. 362 | 363 | Currently `cloudflare`, `akamai`, `incapsula` and `sucuri` IPs are supported for exclusions. 364 | 365 | # Scan Status 366 | Naabu exposes json scan info on a local port bound to localhost at `http://localhost:63636/metrics` (the port can be changed via the `-metrics-port` flag) 367 | 368 | # Using naabu as library 369 | The following sample program scan the port `80` of `scanme.sh`. The results are returned via the `OnResult` callback: 370 | 371 | ```go 372 | package main 373 | 374 | import ( 375 | "log" 376 | 377 | "github.com/projectdiscovery/goflags" 378 | "github.com/projectdiscovery/naabu/v2/pkg/result" 379 | "github.com/projectdiscovery/naabu/v2/pkg/runner" 380 | ) 381 | 382 | func main() { 383 | options := runner.Options{ 384 | Host: goflags.StringSlice{"scanme.sh"}, 385 | ScanType: "s", 386 | OnResult: func(hr *result.HostResult) { 387 | log.Println(hr.Host, hr.Ports) 388 | }, 389 | Ports: "80", 390 | } 391 | 392 | naabuRunner, err := runner.NewRunner(&options) 393 | if err != nil { 394 | log.Fatal(err) 395 | } 396 | defer naabuRunner.Close() 397 | 398 | naabuRunner.RunEnumeration() 399 | } 400 | ``` 401 | 402 | # Notes 403 | 404 | - Naabu allows arbitrary binary execution as a feature to support [nmap integration](https://github.com/projectdiscovery/naabu#nmap-integration). 405 | - Naabu is designed to scan ports on multiple hosts / mass port scanning. 406 | - As default naabu is configured with a assumption that you are running it from VPS. 407 | - We suggest tuning the flags / rate if running naabu from local system. 408 | - For best results, run naabu as **root** user. 409 | 410 | ----- 411 | 412 | Naabu is made with 🖤 by the [projectdiscovery](https://projectdiscovery.io) team. Community contributions have made the project what it is. 413 | 414 | See the **[Thanks.md](https://github.com/projectdiscovery/naabu/blob/master/THANKS.md)** file for more details. 415 | -------------------------------------------------------------------------------- /THANKS.md: -------------------------------------------------------------------------------- 1 | # Thanks 2 | 3 | 1. [Furious](https://github.com/liamg/furious) @liamg - Actual project from where the idea was taken. 4 | 2. [Nmap](https://nmap.org/) - For top port lists and being an amazing tool to infosec community. 5 | 3. [Masscan](https://github.com/robertdavidgraham/masscan) - Awesome and fast scanning tool 6 | 4. [Zmap](https://zmap.io/) - Highly optimized single port scan tool 7 | -------------------------------------------------------------------------------- /cmd/functional-test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strings" 10 | 11 | "github.com/logrusorgru/aurora" 12 | "github.com/pkg/errors" 13 | 14 | "github.com/projectdiscovery/naabu/v2/internal/testutils" 15 | ) 16 | 17 | var ( 18 | debug = os.Getenv("DEBUG") == "true" 19 | success = aurora.Green("[✓]").String() 20 | failed = aurora.Red("[✘]").String() 21 | errored = false 22 | 23 | mainNaabuBinary = flag.String("main", "", "Main Branch Naabu Binary") 24 | devNaabuBinary = flag.String("dev", "", "Dev Branch Naabu Binary") 25 | testcases = flag.String("testcases", "", "Test cases file for Naabu functional tests") 26 | ) 27 | 28 | func main() { 29 | flag.Parse() 30 | 31 | if err := runFunctionalTests(); err != nil { 32 | log.Fatalf("Could not run functional tests: %s\n", err) 33 | } 34 | if errored { 35 | os.Exit(1) 36 | } 37 | } 38 | 39 | func runFunctionalTests() error { 40 | file, err := os.Open(*testcases) 41 | if err != nil { 42 | return errors.Wrap(err, "could not open test cases") 43 | } 44 | defer func() { 45 | if err := file.Close(); err != nil { 46 | log.Printf("could not close test cases file: %s\n", err) 47 | } 48 | }() 49 | 50 | scanner := bufio.NewScanner(file) 51 | for scanner.Scan() { 52 | text := strings.TrimSpace(scanner.Text()) 53 | if text == "" { 54 | continue 55 | } 56 | if err := runIndividualTestCase(text); err != nil { 57 | errored = true 58 | fmt.Fprintf(os.Stderr, "%s Test \"%s\" failed: %s\n", failed, text, err) 59 | } else { 60 | fmt.Printf("%s Test \"%s\" passed!\n", success, text) 61 | } 62 | } 63 | return nil 64 | } 65 | 66 | func runIndividualTestCase(testcase string) error { 67 | parts := strings.Fields(testcase) 68 | 69 | var finalArgs []string 70 | var target string 71 | if len(parts) > 1 { 72 | finalArgs = parts[2:] 73 | target = parts[0] 74 | } 75 | mainOutput, err := testutils.RunNaabuBinaryAndGetResults(target, *mainNaabuBinary, debug, finalArgs) 76 | if err != nil { 77 | return errors.Wrap(err, "could not run naabu main test") 78 | } 79 | devOutput, err := testutils.RunNaabuBinaryAndGetResults(target, *devNaabuBinary, debug, finalArgs) 80 | if err != nil { 81 | return errors.Wrap(err, "could not run naabu dev test") 82 | } 83 | if len(mainOutput) == len(devOutput) { 84 | return nil 85 | } 86 | return fmt.Errorf("%s main is not equal to %s dev", mainOutput, devOutput) 87 | } 88 | -------------------------------------------------------------------------------- /cmd/functional-test/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo 'Building functional-test binary' 4 | go build 5 | 6 | echo 'Building NAABU binary from current branch' 7 | go build -o naabu_dev ../naabu 8 | 9 | echo 'Installing latest release of NAABU' 10 | GO111MODULE=on go build -v github.com/projectdiscovery/naabu/v2/cmd/naabu 11 | 12 | echo 'Starting NAABU functional test' 13 | ./functional-test -main ./naabu -dev ./naabu_dev -testcases testcases.txt 14 | -------------------------------------------------------------------------------- /cmd/functional-test/test-data/request.txt: -------------------------------------------------------------------------------- 1 | 127.0.0.1 2 | -------------------------------------------------------------------------------- /cmd/functional-test/testcases.txt: -------------------------------------------------------------------------------- 1 | 127.0.0.1 {{binary}} -p 8000 2 | 127.0.0.1 {{binary}} -p 8000,443 3 | 127.0.0.1 {{binary}} -tp 100 4 | 127.0.0.1 {{binary}} -ep 80 -p 8000 5 | 127.0.0.1 {{binary}} -c 25 -p 8000 6 | 127.0.0.1 {{binary}} -nmap-cli '-sT' -p 8000 7 | 127.0.0.1 {{binary}} -json 8 | 127.0.0.1 {{binary}} -nmap-cli '-sT' 9 | scanme.sh {{binary}} -stream -passive 10 | scanme.sh {{binary}} -stream -passive -verify 11 | -------------------------------------------------------------------------------- /cmd/integration-test/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/projectdiscovery/naabu/v2/internal/testutils" 5 | ) 6 | 7 | var cliTestcases = map[string]testutils.TestCase{ 8 | "cli with passive flag": &cliWithPassiveFlag{}, 9 | } 10 | 11 | type cliWithPassiveFlag struct { 12 | } 13 | 14 | func (h *cliWithPassiveFlag) Execute() error { 15 | results, err := testutils.RunNaabuAndGetResults("projectdiscovery.io", false, "-ec", "-passive") 16 | if err != nil { 17 | return err 18 | } 19 | 20 | if len(results) <= 0 { 21 | return errIncorrectResultsCount(results) 22 | } 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /cmd/integration-test/integration-test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/logrusorgru/aurora" 9 | "github.com/projectdiscovery/naabu/v2/internal/testutils" 10 | ) 11 | 12 | var ( 13 | // debug = os.Getenv("DEBUG") == "true" // currently not used 14 | customTest = os.Getenv("TEST") 15 | protocol = os.Getenv("PROTO") 16 | 17 | errored = false 18 | ) 19 | 20 | func main() { 21 | success := aurora.Green("[✓]").String() 22 | failed := aurora.Red("[✘]").String() 23 | 24 | tests := map[string]map[string]testutils.TestCase{ 25 | "library": libraryTestcases, 26 | "cli": cliTestcases, 27 | } 28 | for proto, tests := range tests { 29 | if protocol == "" || protocol == proto { 30 | fmt.Printf("Running test cases for \"%s\"\n", aurora.Blue(proto)) 31 | 32 | for name, test := range tests { 33 | if customTest != "" && !strings.Contains(name, customTest) { 34 | continue // only run tests user asked 35 | } 36 | err := test.Execute() 37 | if err != nil { 38 | fmt.Fprintf(os.Stderr, "%s Test \"%s\" failed: %s\n", failed, name, err) 39 | errored = true 40 | } else { 41 | fmt.Printf("%s Test \"%s\" passed!\n", success, name) 42 | } 43 | } 44 | } 45 | } 46 | if errored { 47 | os.Exit(1) 48 | } 49 | } 50 | 51 | func errIncorrectResultsCount(results []string) error { 52 | return fmt.Errorf("incorrect number of results %s", strings.Join(results, "\n\t")) 53 | } 54 | 55 | // Currently not used 56 | // func errIncorrectResult(expected, got string) error { 57 | // return fmt.Errorf("incorrect result: expected \"%s\" got \"%s\"", expected, got) 58 | // } 59 | -------------------------------------------------------------------------------- /cmd/integration-test/library.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | "os" 8 | "os/user" 9 | "time" 10 | 11 | "github.com/armon/go-socks5" 12 | "github.com/projectdiscovery/naabu/v2/internal/testutils" 13 | "github.com/projectdiscovery/naabu/v2/pkg/privileges" 14 | "github.com/projectdiscovery/naabu/v2/pkg/result" 15 | "github.com/projectdiscovery/naabu/v2/pkg/runner" 16 | ) 17 | 18 | var libraryTestcases = map[string]testutils.TestCase{ 19 | "sdk - one passive execution": &naabuPassiveSingleLibrary{}, 20 | "sdk - one execution - connect": &naabuSingleLibrary{scanType: "c"}, 21 | "sdk - multiple executions - connect": &naabuMultipleExecLibrary{scanType: "c"}, 22 | "sdk - one execution - syn": &naabuSingleLibrary{scanType: "s"}, 23 | "sdk - multiple executions - syn": &naabuMultipleExecLibrary{scanType: "s"}, 24 | "sdk - connect with proxy": &naabuWithSocks5{}, 25 | } 26 | 27 | type naabuPassiveSingleLibrary struct { 28 | } 29 | 30 | func (h *naabuPassiveSingleLibrary) Execute() error { 31 | testFile := "test.txt" 32 | err := os.WriteFile(testFile, []byte("scanme.sh"), 0644) 33 | if err != nil { 34 | return err 35 | } 36 | defer func() { 37 | if err := os.RemoveAll(testFile); err != nil { 38 | log.Printf("could not remove test file: %s\n", err) 39 | } 40 | }() 41 | 42 | options := runner.Options{ 43 | HostsFile: testFile, 44 | Ports: "80", 45 | Passive: true, 46 | OnResult: func(hr *result.HostResult) {}, 47 | } 48 | 49 | naabuRunner, err := runner.NewRunner(&options) 50 | if err != nil { 51 | return err 52 | } 53 | defer func() { 54 | if err := naabuRunner.Close(); err != nil { 55 | log.Printf("could not close naabu runner: %s\n", err) 56 | } 57 | }() 58 | 59 | return naabuRunner.RunEnumeration(context.TODO()) 60 | } 61 | 62 | type naabuSingleLibrary struct { 63 | scanType string 64 | } 65 | 66 | func (h *naabuSingleLibrary) Execute() error { 67 | if h.scanType == "s" && !privileges.IsPrivileged { 68 | usr, _ := user.Current() 69 | return errors.New("invalid user" + usr.Name) 70 | } 71 | 72 | testFile := "test.txt" 73 | err := os.WriteFile(testFile, []byte("scanme.sh"), 0644) 74 | if err != nil { 75 | return err 76 | } 77 | defer func() { 78 | if err := os.RemoveAll(testFile); err != nil { 79 | log.Printf("could not remove test file: %s\n", err) 80 | } 81 | }() 82 | 83 | var got bool 84 | 85 | options := runner.Options{ 86 | HostsFile: testFile, 87 | Ports: "80", 88 | ScanType: h.scanType, 89 | OnResult: func(hr *result.HostResult) { 90 | got = true 91 | }, 92 | WarmUpTime: 2, 93 | } 94 | 95 | naabuRunner, err := runner.NewRunner(&options) 96 | if err != nil { 97 | return err 98 | } 99 | defer func() { 100 | if err := naabuRunner.Close(); err != nil { 101 | log.Printf("could not close naabu runner: %s\n", err) 102 | } 103 | }() 104 | 105 | if err = naabuRunner.RunEnumeration(context.TODO()); err != nil { 106 | return err 107 | } 108 | if !got { 109 | return errors.New("no results found") 110 | } 111 | 112 | return nil 113 | } 114 | 115 | type naabuMultipleExecLibrary struct { 116 | scanType string 117 | } 118 | 119 | func (h *naabuMultipleExecLibrary) Execute() error { 120 | if h.scanType == "s" && !privileges.IsPrivileged { 121 | usr, _ := user.Current() 122 | return errors.New("invalid user" + usr.Name) 123 | } 124 | 125 | testFile := "test.txt" 126 | err := os.WriteFile(testFile, []byte("scanme.sh"), 0644) 127 | if err != nil { 128 | return err 129 | } 130 | defer func() { 131 | if err := os.RemoveAll(testFile); err != nil { 132 | log.Printf("could not remove test file: %s\n", err) 133 | } 134 | }() 135 | 136 | var got bool 137 | 138 | options := runner.Options{ 139 | HostsFile: testFile, 140 | Ports: "80", 141 | ScanType: h.scanType, 142 | OnResult: func(hr *result.HostResult) { 143 | got = true 144 | }, 145 | WarmUpTime: 2, 146 | } 147 | 148 | for i := 0; i < 3; i++ { 149 | naabuRunner, err := runner.NewRunner(&options) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | if err = naabuRunner.RunEnumeration(context.TODO()); err != nil { 155 | return err 156 | } 157 | if !got { 158 | return errors.New("no results found") 159 | } 160 | if err := naabuRunner.Close(); err != nil { 161 | log.Printf("could not close naabu runner: %s\n", err) 162 | } 163 | } 164 | return nil 165 | } 166 | 167 | type naabuWithSocks5 struct{} 168 | 169 | func (h *naabuWithSocks5) Execute() error { 170 | // Start local SOCKS5 proxy server with test:test credentials 171 | conf := &socks5.Config{ 172 | Credentials: socks5.StaticCredentials{ 173 | "test": "test", 174 | }, 175 | } 176 | server, err := socks5.New(conf) 177 | if err != nil { 178 | panic(err) 179 | } 180 | go func() { 181 | if err = server.ListenAndServe("tcp", "127.0.0.1:38401"); err != nil { 182 | panic(err) 183 | } 184 | }() 185 | 186 | testFile := "test.txt" 187 | err = os.WriteFile(testFile, []byte("scanme.sh"), 0644) 188 | if err != nil { 189 | return err 190 | } 191 | defer func() { 192 | if err := os.RemoveAll(testFile); err != nil { 193 | log.Printf("could not remove test file: %s\n", err) 194 | } 195 | }() 196 | 197 | var got bool 198 | 199 | options := runner.Options{ 200 | HostsFile: testFile, 201 | Ports: "80", 202 | ScanType: "c", 203 | Proxy: "127.0.0.1:38401", 204 | ProxyAuth: "test:test", 205 | OnResult: func(hr *result.HostResult) { 206 | got = true 207 | }, 208 | WarmUpTime: 2, 209 | Timeout: 10 * time.Second, 210 | } 211 | 212 | naabuRunner, err := runner.NewRunner(&options) 213 | if err != nil { 214 | return err 215 | } 216 | defer func() { 217 | if err := naabuRunner.Close(); err != nil { 218 | log.Printf("could not close naabu runner: %s\n", err) 219 | } 220 | }() 221 | 222 | if err = naabuRunner.RunEnumeration(context.TODO()); err != nil { 223 | return err 224 | } 225 | if !got { 226 | return errors.New("no results found") 227 | } 228 | 229 | return nil 230 | } 231 | -------------------------------------------------------------------------------- /cmd/naabu/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/logrusorgru/aurora" 11 | _ "github.com/projectdiscovery/fdmax/autofdmax" 12 | "github.com/projectdiscovery/gologger" 13 | "github.com/projectdiscovery/naabu/v2/internal/pdcp" 14 | "github.com/projectdiscovery/naabu/v2/pkg/port" 15 | "github.com/projectdiscovery/naabu/v2/pkg/protocol" 16 | "github.com/projectdiscovery/naabu/v2/pkg/result" 17 | "github.com/projectdiscovery/naabu/v2/pkg/runner" 18 | pdcpauth "github.com/projectdiscovery/utils/auth/pdcp" 19 | ) 20 | 21 | func main() { 22 | // Parse the command line flags and read config files 23 | options := runner.ParseOptions() 24 | 25 | // validation for local results file upload 26 | if options.AssetFileUpload != "" { 27 | _ = setupOptionalAssetUpload(options) 28 | file, err := os.Open(options.AssetFileUpload) 29 | if err != nil { 30 | gologger.Fatal().Msgf("Could not open file: %s\n", err) 31 | } 32 | defer func() { 33 | if err := file.Close(); err != nil { 34 | gologger.Error().Msgf("Could not close file: %s\n", err) 35 | } 36 | }() 37 | dec := json.NewDecoder(file) 38 | for dec.More() { 39 | var r runner.Result 40 | err := dec.Decode(&r) 41 | if err != nil { 42 | gologger.Fatal().Msgf("Could not decode jsonl file: %s\n", err) 43 | } 44 | options.OnResult(&result.HostResult{ 45 | Host: r.Host, 46 | IP: r.IP, 47 | Ports: []*port.Port{ 48 | { 49 | Port: r.Port, 50 | Protocol: protocol.ParseProtocol(r.Protocol), 51 | TLS: r.TLS, 52 | }, 53 | }, 54 | }) 55 | } 56 | options.OnClose() 57 | return 58 | } 59 | 60 | // setup optional asset upload 61 | _ = setupOptionalAssetUpload(options) 62 | 63 | naabuRunner, err := runner.NewRunner(options) 64 | if err != nil { 65 | gologger.Fatal().Msgf("Could not create runner: %s\n", err) 66 | } 67 | 68 | // Setup context with cancelation 69 | ctx, cancel := context.WithCancel(context.Background()) 70 | 71 | // Setup signal handling 72 | c := make(chan os.Signal, 1) 73 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 74 | go func() { 75 | sig := <-c 76 | gologger.Info().Msgf("Received signal: %s, exiting gracefully...\n", sig) 77 | 78 | // Cancel context to stop ongoing tasks 79 | cancel() 80 | 81 | // Try to save resume config if needed 82 | if options.ResumeCfg != nil && options.ResumeCfg.ShouldSaveResume() { 83 | gologger.Info().Msgf("Creating resume file: %s\n", runner.DefaultResumeFilePath()) 84 | if err := options.ResumeCfg.SaveResumeConfig(); err != nil { 85 | gologger.Error().Msgf("Couldn't create resume file: %s\n", err) 86 | } 87 | } 88 | 89 | // Show scan result if runner is available 90 | if naabuRunner != nil { 91 | naabuRunner.ShowScanResultOnExit() 92 | 93 | if err := naabuRunner.Close(); err != nil { 94 | gologger.Error().Msgf("Couldn't close runner: %s\n", err) 95 | } 96 | } 97 | 98 | // Final flush if gologger has a Close method (placeholder if exists) 99 | // Example: gologger.Close() 100 | 101 | os.Exit(1) 102 | }() 103 | 104 | // Start enumeration 105 | if err := naabuRunner.RunEnumeration(ctx); err != nil { 106 | gologger.Fatal().Msgf("Could not run enumeration: %s\n", err) 107 | } 108 | 109 | defer func() { 110 | if err := naabuRunner.Close(); err != nil { 111 | gologger.Error().Msgf("Couldn't close runner: %s\n", err) 112 | } 113 | // On successful execution, cleanup resume config if needed 114 | if options.ResumeCfg != nil { 115 | options.ResumeCfg.CleanupResumeConfig() 116 | } 117 | }() 118 | } 119 | 120 | // setupOptionalAssetUpload is used to setup optional asset upload 121 | // this is optional and only initialized when explicitly enabled 122 | func setupOptionalAssetUpload(opts *runner.Options) *pdcp.UploadWriter { 123 | var mustEnable bool 124 | // enable on multiple conditions 125 | if opts.AssetUpload || opts.AssetID != "" || opts.AssetName != "" || pdcp.EnableCloudUpload { 126 | mustEnable = true 127 | } 128 | a := aurora.NewAurora(!opts.NoColor) 129 | if !mustEnable { 130 | if !pdcp.HideAutoSaveMsg { 131 | gologger.Print().Msgf("[%s] UI Dashboard is disabled, Use -dashboard option to enable", a.BrightYellow("WRN")) 132 | } 133 | return nil 134 | } 135 | 136 | gologger.Info().Msgf("To view results in UI dashboard, visit https://cloud.projectdiscovery.io/assets upon completion.") 137 | h := &pdcpauth.PDCPCredHandler{} 138 | creds, err := h.GetCreds() 139 | if err != nil { 140 | if err != pdcpauth.ErrNoCreds && !pdcp.HideAutoSaveMsg { 141 | gologger.Verbose().Msgf("Could not get credentials for cloud upload: %s\n", err) 142 | } 143 | pdcpauth.CheckNValidateCredentials("naabu") 144 | return nil 145 | } 146 | writer, err := pdcp.NewUploadWriterCallback(context.Background(), creds) 147 | if err != nil { 148 | gologger.Error().Msgf("failed to setup UI dashboard: %s", err) 149 | return nil 150 | } 151 | if writer == nil { 152 | gologger.Error().Msgf("something went wrong, could not setup UI dashboard") 153 | } 154 | opts.OnResult = writer.GetWriterCallback() 155 | opts.OnClose = func() { 156 | writer.Close() 157 | } 158 | // add additional metadata 159 | if opts.AssetID != "" { 160 | // silently ignore 161 | _ = writer.SetAssetID(opts.AssetID) 162 | } 163 | if opts.AssetName != "" { 164 | // silently ignore 165 | writer.SetAssetGroupName(opts.AssetName) 166 | } 167 | if opts.TeamID != "" { 168 | writer.SetTeamID(opts.TeamID) 169 | } 170 | return writer 171 | } 172 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/projectdiscovery/naabu/v2 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 7 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 8 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 9 | github.com/gopacket/gopacket v1.2.0 10 | github.com/logrusorgru/aurora v2.0.3+incompatible 11 | github.com/miekg/dns v1.1.62 12 | github.com/pkg/errors v0.9.1 13 | github.com/projectdiscovery/blackrock v0.0.1 14 | github.com/projectdiscovery/cdncheck v1.1.20 15 | github.com/projectdiscovery/clistats v0.1.1 16 | github.com/projectdiscovery/dnsx v1.2.2 17 | github.com/projectdiscovery/fdmax v0.0.4 18 | github.com/projectdiscovery/freeport v0.0.7 19 | github.com/projectdiscovery/goflags v0.1.74 20 | github.com/projectdiscovery/gologger v1.1.49 21 | github.com/projectdiscovery/ipranger v0.0.53 22 | github.com/projectdiscovery/mapcidr v1.1.34 23 | github.com/projectdiscovery/networkpolicy v0.1.15 24 | github.com/projectdiscovery/ratelimit v0.0.81 25 | github.com/projectdiscovery/retryablehttp-go v1.0.113 26 | github.com/projectdiscovery/uncover v1.0.9 27 | github.com/projectdiscovery/utils v0.4.19 28 | github.com/remeh/sizedwaitgroup v1.0.0 29 | github.com/stretchr/testify v1.10.0 30 | go.uber.org/multierr v1.11.0 31 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 32 | golang.org/x/net v0.38.0 33 | golang.org/x/sys v0.31.0 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/akrylysov/pogreb v0.10.1 // indirect 42 | github.com/alecthomas/chroma/v2 v2.14.0 // indirect 43 | github.com/andybalholm/brotli v1.0.6 // indirect 44 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 45 | github.com/aymerick/douceur v0.2.0 // indirect 46 | github.com/charmbracelet/glamour v0.8.0 // indirect 47 | github.com/charmbracelet/lipgloss v0.13.0 // indirect 48 | github.com/charmbracelet/x/ansi v0.3.2 // indirect 49 | github.com/cheggaaa/pb/v3 v3.1.4 // indirect 50 | github.com/cloudflare/circl v1.5.0 // indirect 51 | github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect 52 | github.com/davecgh/go-spew v1.1.1 // indirect 53 | github.com/dimchansky/utfbom v1.1.1 // indirect 54 | github.com/dlclark/regexp2 v1.11.4 // indirect 55 | github.com/docker/go-units v0.5.0 // indirect 56 | github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect 57 | github.com/fatih/color v1.15.0 // indirect 58 | github.com/gaissmai/bart v0.20.4 // indirect 59 | github.com/go-ole/go-ole v1.2.6 // indirect 60 | github.com/golang/protobuf v1.5.4 // indirect 61 | github.com/golang/snappy v0.0.4 // indirect 62 | github.com/google/go-github/v30 v30.1.0 // indirect 63 | github.com/google/go-querystring v1.1.0 // indirect 64 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 65 | github.com/google/uuid v1.3.1 // indirect 66 | github.com/gorilla/css v1.0.1 // indirect 67 | github.com/json-iterator/go v1.1.12 // indirect 68 | github.com/klauspost/compress v1.17.4 // indirect 69 | github.com/klauspost/pgzip v1.2.5 // indirect 70 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 71 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 72 | github.com/mattn/go-colorable v0.1.13 // indirect 73 | github.com/mattn/go-isatty v0.0.20 // indirect 74 | github.com/mattn/go-runewidth v0.0.16 // indirect 75 | github.com/mholt/archiver/v3 v3.5.1 // indirect 76 | github.com/microcosm-cc/bluemonday v1.0.27 // indirect 77 | github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 // indirect 78 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 79 | github.com/modern-go/reflect2 v1.0.2 // indirect 80 | github.com/muesli/reflow v0.3.0 // indirect 81 | github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect 82 | github.com/nwaples/rardecode v1.1.3 // indirect 83 | github.com/pierrec/lz4/v4 v4.1.2 // indirect 84 | github.com/pmezard/go-difflib v1.0.0 // indirect 85 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 86 | github.com/projectdiscovery/asnmap v1.1.1 // indirect 87 | github.com/projectdiscovery/fastdialer v0.4.0 // indirect 88 | github.com/projectdiscovery/hmap v0.0.89 // indirect 89 | github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983 // indirect 90 | github.com/projectdiscovery/retryabledns v1.0.100 // indirect 91 | github.com/refraction-networking/utls v1.7.0 // indirect 92 | github.com/rivo/uniseg v0.4.7 // indirect 93 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect 94 | github.com/shirou/gopsutil/v3 v3.23.7 // indirect 95 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 96 | github.com/syndtr/goleveldb v1.0.0 // indirect 97 | github.com/tidwall/btree v1.6.0 // indirect 98 | github.com/tidwall/buntdb v1.3.0 // indirect 99 | github.com/tidwall/gjson v1.18.0 // indirect 100 | github.com/tidwall/grect v0.1.4 // indirect 101 | github.com/tidwall/match v1.1.1 // indirect 102 | github.com/tidwall/pretty v1.2.1 // indirect 103 | github.com/tidwall/rtred v0.1.2 // indirect 104 | github.com/tidwall/tinyqueue v0.1.1 // indirect 105 | github.com/tklauser/go-sysconf v0.3.12 // indirect 106 | github.com/tklauser/numcpus v0.6.1 // indirect 107 | github.com/ulikunitz/xz v0.5.11 // indirect 108 | github.com/weppos/publicsuffix-go v0.30.2-0.20230730094716-a20f9abcc222 // indirect 109 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 110 | github.com/yl2chen/cidranger v1.0.2 // indirect 111 | github.com/yuin/goldmark v1.7.4 // indirect 112 | github.com/yuin/goldmark-emoji v1.0.3 // indirect 113 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 114 | github.com/zcalusic/sysinfo v1.0.2 // indirect 115 | github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 // indirect 116 | github.com/zmap/zcrypto v0.0.0-20230814193918-dbe676986518 // indirect 117 | go.etcd.io/bbolt v1.3.7 // indirect 118 | golang.org/x/crypto v0.36.0 // indirect 119 | golang.org/x/mod v0.22.0 // indirect 120 | golang.org/x/oauth2 v0.18.0 // indirect 121 | golang.org/x/sync v0.12.0 // indirect 122 | golang.org/x/term v0.30.0 // indirect 123 | golang.org/x/text v0.23.0 // indirect 124 | golang.org/x/time v0.5.0 // indirect 125 | golang.org/x/tools v0.29.0 // indirect 126 | google.golang.org/appengine v1.6.8 // indirect 127 | google.golang.org/protobuf v1.33.0 // indirect 128 | gopkg.in/djherbis/times.v1 v1.3.0 // indirect 129 | gopkg.in/yaml.v3 v3.0.1 // indirect 130 | ) 131 | -------------------------------------------------------------------------------- /integration_tests/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "::group::Build naabu" 4 | rm integration-test naabu 2>/dev/null 5 | cd ../cmd/naabu 6 | go build 7 | mv naabu ../../integration_tests/naabu 8 | echo "::endgroup::" 9 | 10 | echo "::group::Build naabu integration-test" 11 | cd ../integration-test 12 | go build 13 | mv integration-test ../../integration_tests/integration-test 14 | cd ../../integration_tests 15 | echo "::endgroup::" 16 | 17 | sudo ./integration-test 18 | if [ $? -eq 0 ] 19 | then 20 | exit 0 21 | else 22 | exit 1 23 | fi 24 | -------------------------------------------------------------------------------- /internal/pdcp/utils.go: -------------------------------------------------------------------------------- 1 | package pdcp 2 | 3 | import ( 4 | pdcpauth "github.com/projectdiscovery/utils/auth/pdcp" 5 | urlutil "github.com/projectdiscovery/utils/url" 6 | ) 7 | 8 | func getAssetsDashBoardURL(id, teamID string) string { 9 | ux, _ := urlutil.Parse(pdcpauth.DashBoardURL) 10 | ux.Path = "/assets/" + id 11 | if ux.Params == nil { 12 | ux.Params = urlutil.NewOrderedParams() 13 | } 14 | if teamID != "" { 15 | ux.Params.Add("team_id", teamID) 16 | } else { 17 | ux.Params.Add("team_id", NoneTeamID) 18 | } 19 | ux.Update() 20 | return ux.String() 21 | } 22 | 23 | // {"asset_id":"cqdtekhte9oc73e9hrvg","message":"Successfully uploaded asset","upload_status":"success","uploaded_at":"2024-07-20 15:27:16.148527329 +0000 UTC m=+1078.215945902"} 24 | type uploadResponse struct { 25 | ID string `json:"asset_id"` 26 | Message string `json:"message"` 27 | } 28 | -------------------------------------------------------------------------------- /internal/pdcp/writer.go: -------------------------------------------------------------------------------- 1 | package pdcp 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "regexp" 12 | "sync/atomic" 13 | "time" 14 | 15 | "github.com/projectdiscovery/gologger" 16 | "github.com/projectdiscovery/naabu/v2/pkg/result" 17 | "github.com/projectdiscovery/naabu/v2/pkg/runner" 18 | "github.com/projectdiscovery/retryablehttp-go" 19 | pdcpauth "github.com/projectdiscovery/utils/auth/pdcp" 20 | "github.com/projectdiscovery/utils/conversion" 21 | "github.com/projectdiscovery/utils/env" 22 | errorutil "github.com/projectdiscovery/utils/errors" 23 | unitutils "github.com/projectdiscovery/utils/unit" 24 | updateutils "github.com/projectdiscovery/utils/update" 25 | urlutil "github.com/projectdiscovery/utils/url" 26 | ) 27 | 28 | const ( 29 | uploadEndpoint = "/v1/assets" 30 | appendEndpoint = "/v1/assets/%s/contents" 31 | flushTimer = time.Minute 32 | MaxChunkSize = 4 * unitutils.Mega // 4 MB 33 | xidRe = `^[a-z0-9]{20}$` 34 | teamIDHeader = "X-Team-Id" 35 | NoneTeamID = "none" 36 | ) 37 | 38 | var ( 39 | xidRegex = regexp.MustCompile(xidRe) 40 | // EnableeUpload if set to true enables the upload feature 41 | HideAutoSaveMsg = env.GetEnvOrDefault("DISABLE_CLOUD_UPLOAD_WRN", false) 42 | EnableCloudUpload = env.GetEnvOrDefault("ENABLE_CLOUD_UPLOAD", false) 43 | ) 44 | 45 | // UploadWriter is a writer that uploads its output to pdcp 46 | // server to enable web dashboard and more 47 | type UploadWriter struct { 48 | creds *pdcpauth.PDCPCredentials 49 | uploadURL *url.URL 50 | client *retryablehttp.Client 51 | done chan struct{} 52 | data chan result.HostResult 53 | assetGroupID string 54 | assetGroupName string 55 | counter atomic.Int32 56 | closed atomic.Bool 57 | TeamID string 58 | } 59 | 60 | // NewUploadWriterCallback creates a new upload writer callback 61 | // which when enabled periodically uploads the results to pdcp assets dashboard 62 | func NewUploadWriterCallback(ctx context.Context, creds *pdcpauth.PDCPCredentials) (*UploadWriter, error) { 63 | if creds == nil { 64 | return nil, fmt.Errorf("no credentials provided") 65 | } 66 | u := &UploadWriter{ 67 | creds: creds, 68 | done: make(chan struct{}, 1), 69 | data: make(chan result.HostResult, 8), // default buffer size 70 | TeamID: "", 71 | } 72 | var err error 73 | tmp, err := urlutil.Parse(creds.Server) 74 | if err != nil { 75 | return nil, errorutil.NewWithErr(err).Msgf("could not parse server url") 76 | } 77 | tmp.Path = uploadEndpoint 78 | tmp.Update() 79 | u.uploadURL = tmp.URL 80 | 81 | // create http client 82 | opts := retryablehttp.DefaultOptionsSingle 83 | opts.NoAdjustTimeout = true 84 | opts.Timeout = time.Duration(3) * time.Minute 85 | u.client = retryablehttp.NewClient(opts) 86 | // start auto commit 87 | // upload every 1 minute or when buffer is full 88 | go u.autoCommit(ctx) 89 | return u, nil 90 | } 91 | 92 | // GetWriterCallback returns the writer callback 93 | func (u *UploadWriter) GetWriterCallback() result.ResultFn { 94 | return func(hr *result.HostResult) { 95 | u.data <- *hr 96 | } 97 | } 98 | 99 | // SetAssetID sets the scan id for the upload writer 100 | func (u *UploadWriter) SetAssetID(id string) error { 101 | if !xidRegex.MatchString(id) { 102 | return fmt.Errorf("invalid asset id provided") 103 | } 104 | u.assetGroupID = id 105 | return nil 106 | } 107 | 108 | // SetAssetGroupName sets the scan name for the upload writer 109 | func (u *UploadWriter) SetAssetGroupName(name string) { 110 | u.assetGroupName = name 111 | } 112 | 113 | // SetTeamID sets the team id for the upload writer 114 | func (u *UploadWriter) SetTeamID(id string) { 115 | u.TeamID = id 116 | } 117 | 118 | func (u *UploadWriter) autoCommit(ctx context.Context) { 119 | // wait for context to be done 120 | defer func() { 121 | u.done <- struct{}{} 122 | close(u.done) 123 | // if no scanid is generated no results were uploaded 124 | if u.assetGroupID == "" { 125 | gologger.Verbose().Msgf("UI dashboard setup skipped, no results found to upload") 126 | } else { 127 | gologger.Info().Msgf("Found %v results, View found results in dashboard : %v", u.counter.Load(), getAssetsDashBoardURL(u.assetGroupID, u.TeamID)) 128 | } 129 | }() 130 | // temporary buffer to store the results 131 | buff := &bytes.Buffer{} 132 | ticker := time.NewTicker(flushTimer) 133 | 134 | for { 135 | select { 136 | case <-ctx.Done(): 137 | // flush before exit 138 | if buff.Len() > 0 { 139 | if err := u.uploadChunk(buff); err != nil { 140 | gologger.Error().Msgf("Failed to upload scan results on cloud: %v", err) 141 | } 142 | } 143 | return 144 | case <-ticker.C: 145 | // flush the buffer 146 | if buff.Len() > 0 { 147 | if err := u.uploadChunk(buff); err != nil { 148 | gologger.Error().Msgf("Failed to upload scan results on cloud: %v", err) 149 | } 150 | } 151 | case res, ok := <-u.data: 152 | if !ok { 153 | if buff.Len() > 0 { 154 | if err := u.uploadChunk(buff); err != nil { 155 | gologger.Error().Msgf("Failed to upload scan results on cloud: %v", err) 156 | } 157 | } 158 | return 159 | } 160 | 161 | lineBytes, err := json.Marshal(res) 162 | if err != nil { 163 | gologger.Error().Msgf("Failed to marshal result: %v", err) 164 | continue 165 | } 166 | u.counter.Add(1) 167 | line := conversion.String(lineBytes) 168 | if buff.Len()+len(line) > MaxChunkSize { 169 | // flush existing buffer 170 | if err := u.uploadChunk(buff); err != nil { 171 | gologger.Error().Msgf("Failed to upload asset results on cloud: %v", err) 172 | } 173 | } else { 174 | buff.WriteString(line) 175 | buff.WriteString("\n") 176 | } 177 | } 178 | } 179 | } 180 | 181 | // uploadChunk uploads a chunk of data to the server 182 | func (u *UploadWriter) uploadChunk(buff *bytes.Buffer) error { 183 | if err := u.upload(buff.Bytes()); err != nil { 184 | return errorutil.NewWithErr(err).Msgf("could not upload chunk") 185 | } 186 | // if successful, reset the buffer 187 | buff.Reset() 188 | // log in verbose mode 189 | gologger.Warning().Msgf("Uploaded results chunk, you can view assets at %v", getAssetsDashBoardURL(u.assetGroupID, u.TeamID)) 190 | return nil 191 | } 192 | 193 | func (u *UploadWriter) upload(data []byte) error { 194 | fmt.Println("upload:", string(data)) 195 | req, err := u.getRequest(data) 196 | if err != nil { 197 | return errorutil.NewWithErr(err).Msgf("could not create upload request") 198 | } 199 | resp, err := u.client.Do(req) 200 | if err != nil { 201 | return errorutil.NewWithErr(err).Msgf("could not upload results") 202 | } 203 | defer func() { 204 | _ = resp.Body.Close() 205 | }() 206 | bin, err := io.ReadAll(resp.Body) 207 | if err != nil { 208 | return errorutil.NewWithErr(err).Msgf("could not get id from response") 209 | } 210 | if resp.StatusCode != http.StatusOK { 211 | return fmt.Errorf("could not upload results got status code %v on %v", resp.StatusCode, resp.Request.URL.String()) 212 | } 213 | var uploadResp uploadResponse 214 | if err := json.Unmarshal(bin, &uploadResp); err != nil { 215 | return errorutil.NewWithErr(err).Msgf("could not unmarshal response got %v", string(bin)) 216 | } 217 | if uploadResp.ID != "" && u.assetGroupID == "" { 218 | u.assetGroupID = uploadResp.ID 219 | } 220 | return nil 221 | } 222 | 223 | // getRequest returns a new request for upload 224 | // if scanID is not provided create new scan by uploading the data 225 | // if scanID is provided append the data to existing scan 226 | func (u *UploadWriter) getRequest(bin []byte) (*retryablehttp.Request, error) { 227 | var method, url string 228 | 229 | if u.assetGroupID == "" { 230 | u.uploadURL.Path = uploadEndpoint 231 | method = http.MethodPost 232 | url = u.uploadURL.String() 233 | } else { 234 | u.uploadURL.Path = fmt.Sprintf(appendEndpoint, u.assetGroupID) 235 | method = http.MethodPatch 236 | url = u.uploadURL.String() 237 | } 238 | req, err := retryablehttp.NewRequest(method, url, bytes.NewReader(bin)) 239 | if err != nil { 240 | return nil, errorutil.NewWithErr(err).Msgf("could not create cloud upload request") 241 | } 242 | // add pdtm meta params 243 | req.Params.Merge(updateutils.GetpdtmParams(runner.Version)) 244 | // if it is upload endpoint also include name if it exists 245 | if u.assetGroupName != "" && req.Path == uploadEndpoint { 246 | req.Params.Add("name", u.assetGroupName) 247 | } 248 | req.Update() 249 | 250 | req.Header.Set(pdcpauth.ApiKeyHeaderName, u.creds.APIKey) 251 | if u.TeamID != "" { 252 | req.Header.Set(teamIDHeader, u.TeamID) 253 | } 254 | req.Header.Set("Content-Type", "application/octet-stream") 255 | req.Header.Set("Accept", "application/json") 256 | return req, nil 257 | } 258 | 259 | // Close closes the upload writer 260 | func (u *UploadWriter) Close() { 261 | if !u.closed.Load() { 262 | // protect to avoid channel closed twice error 263 | close(u.data) 264 | u.closed.Store(true) 265 | } 266 | <-u.done 267 | } 268 | -------------------------------------------------------------------------------- /internal/testutils/integration.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | // RunNaabuAndGetResults returns a list of results 11 | func RunNaabuAndGetResults(question string, debug bool, extra ...string) ([]string, error) { 12 | cmd := exec.Command("bash", "-c") 13 | cmdLine := `echo "` + question + `" | ./naabu ` 14 | cmdLine += strings.Join(extra, " ") 15 | if debug { 16 | cmdLine += " -debug" 17 | cmd.Stderr = os.Stderr 18 | } else { 19 | cmdLine += " -silent" 20 | } 21 | 22 | cmd.Args = append(cmd.Args, cmdLine) 23 | 24 | data, err := cmd.Output() 25 | if err != nil { 26 | return nil, err 27 | } 28 | parts := []string{} 29 | items := strings.Split(string(data), "\n") 30 | for _, i := range items { 31 | if i != "" { 32 | parts = append(parts, i) 33 | } 34 | } 35 | return parts, nil 36 | } 37 | func RunNaabuBinaryAndGetResults(target string, naabuBinary string, debug bool, args []string) ([]string, error) { 38 | cmd := exec.Command("bash", "-c") 39 | cmdLine := fmt.Sprintf(`echo %s | %s `, target, naabuBinary) 40 | cmdLine += strings.Join(args, " ") 41 | if debug { 42 | cmdLine += " -debug" 43 | cmd.Stderr = os.Stderr 44 | } else { 45 | cmdLine += " -silent" 46 | } 47 | 48 | cmd.Args = append(cmd.Args, cmdLine) 49 | data, err := cmd.Output() 50 | if err != nil { 51 | return nil, err 52 | } 53 | parts := []string{} 54 | items := strings.Split(string(data), "\n") 55 | for _, i := range items { 56 | if i != "" { 57 | parts = append(parts, i) 58 | } 59 | } 60 | return parts, nil 61 | } 62 | 63 | // TestCase is a single integration test case 64 | type TestCase interface { 65 | // Execute executes a test case and returns any errors if occurred 66 | Execute() error 67 | } 68 | -------------------------------------------------------------------------------- /pkg/israce/norace.go: -------------------------------------------------------------------------------- 1 | //go:build !race 2 | 3 | // Package israce reports if the Go race detector is enabled. 4 | package israce 5 | 6 | // Enabled reports if the race detector is enabled. 7 | const Enabled = false 8 | -------------------------------------------------------------------------------- /pkg/israce/race.go: -------------------------------------------------------------------------------- 1 | //go:build race 2 | 3 | // Package israce reports if the Go race detector is enabled. 4 | package israce 5 | 6 | // Enabled reports if the race detector is enabled. 7 | const Enabled = true 8 | -------------------------------------------------------------------------------- /pkg/port/port.go: -------------------------------------------------------------------------------- 1 | package port 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/projectdiscovery/naabu/v2/pkg/protocol" 8 | ) 9 | 10 | type Port struct { 11 | Port int `json:"port"` 12 | Protocol protocol.Protocol `json:"protocol"` 13 | 14 | // Deprecated: TLS field will be removed in a future version 15 | TLS bool `json:"tls"` 16 | Service *Service `json:"service,omitempty"` 17 | } 18 | 19 | func (p *Port) String() string { 20 | return fmt.Sprintf("%d", p.Port) 21 | } 22 | 23 | func (p *Port) StringWithDetails() string { 24 | var builder strings.Builder 25 | builder.WriteString(fmt.Sprintf("%d", p.Port)) 26 | builder.WriteString(" [") 27 | builder.WriteString(p.Protocol.String()) 28 | if p.TLS { 29 | builder.WriteString("/tls") 30 | } 31 | builder.WriteString("]") 32 | return builder.String() 33 | } 34 | 35 | type Service struct { 36 | DeviceType string `json:"device_type,omitempty"` 37 | ExtraInfo string `json:"extra_info,omitempty"` 38 | HighVersion string `json:"high_version,omitempty"` 39 | Hostname string `json:"hostname,omitempty"` 40 | LowVersion string `json:"low_version,omitempty"` 41 | Method string `json:"method,omitempty"` 42 | Name string `json:"name,omitempty"` 43 | OSType string `json:"os_type,omitempty"` 44 | Product string `json:"product,omitempty"` 45 | Proto string `json:"proto,omitempty"` 46 | RPCNum string `json:"rpc_num,omitempty"` 47 | ServiceFP string `json:"service_fp,omitempty"` 48 | Tunnel string `json:"tunnel,omitempty"` 49 | Version string `json:"version,omitempty"` 50 | Confidence int `json:"confidence,omitempty"` 51 | } 52 | 53 | func (s *Service) String() string { 54 | return s.Name 55 | } 56 | -------------------------------------------------------------------------------- /pkg/privileges/privileges.go: -------------------------------------------------------------------------------- 1 | package privileges 2 | 3 | var IsPrivileged bool 4 | 5 | func init() { 6 | IsPrivileged = isPrivileged() 7 | } 8 | -------------------------------------------------------------------------------- /pkg/privileges/privileges_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package privileges 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | // isPrivileged checks if the current process has the CAP_NET_RAW capability or is root 10 | func isPrivileged() bool { 11 | return os.Geteuid() == 0 12 | } 13 | -------------------------------------------------------------------------------- /pkg/privileges/privileges_freebsd.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd 2 | 3 | package privileges 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | // isPrivileged checks if the current process is root 10 | func isPrivileged() bool { 11 | return os.Geteuid() == 0 12 | } 13 | -------------------------------------------------------------------------------- /pkg/privileges/privileges_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux || unix 2 | 3 | package privileges 4 | 5 | import ( 6 | "os" 7 | "runtime" 8 | 9 | "github.com/projectdiscovery/naabu/v2/pkg/israce" 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | // isPrivileged checks if the current process has the CAP_NET_RAW capability or is root 14 | func isPrivileged() bool { 15 | // runtime.LockOSThread interferes with race detection 16 | if !israce.Enabled { 17 | header := unix.CapUserHeader{ 18 | Version: unix.LINUX_CAPABILITY_VERSION_3, 19 | Pid: int32(os.Getpid()), 20 | } 21 | data := unix.CapUserData{} 22 | runtime.LockOSThread() 23 | defer runtime.UnlockOSThread() 24 | 25 | if err := unix.Capget(&header, &data); err == nil { 26 | data.Inheritable = (1 << unix.CAP_NET_RAW) 27 | 28 | if err := unix.Capset(&header, &data); err == nil { 29 | return true 30 | } 31 | } 32 | } 33 | return os.Geteuid() == 0 34 | } 35 | -------------------------------------------------------------------------------- /pkg/privileges/privileges_openbsd.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd 2 | 3 | package privileges 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | // isPrivileged checks if the current process is root 10 | func isPrivileged() bool { 11 | return os.Geteuid() == 0 12 | } 13 | -------------------------------------------------------------------------------- /pkg/privileges/privileges_win.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package privileges 4 | 5 | // IsPrivileged on windows doesn't matter as we are using connect scan 6 | func isPrivileged() bool { 7 | return false 8 | } 9 | -------------------------------------------------------------------------------- /pkg/protocol/protocol.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Protocol int 8 | 9 | const ( 10 | TCP Protocol = iota 11 | UDP 12 | ARP 13 | ) 14 | 15 | func (p Protocol) String() string { 16 | switch p { 17 | case TCP: 18 | return "tcp" 19 | case UDP: 20 | return "udp" 21 | case ARP: 22 | return "arp" 23 | default: 24 | panic("uknown type") 25 | } 26 | } 27 | 28 | func ParseProtocol(s string) Protocol { 29 | switch s { 30 | case "tcp": 31 | return TCP 32 | case "udp": 33 | return UDP 34 | case "arp": 35 | return ARP 36 | default: 37 | panic("uknown type") 38 | } 39 | } 40 | 41 | func (p Protocol) MarshalJSON() ([]byte, error) { 42 | return []byte(`"` + p.String() + `"`), nil 43 | } 44 | 45 | func (p *Protocol) UnmarshalJSON(data []byte) error { 46 | // Remove quotes from string 47 | s := string(data) 48 | if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { 49 | return fmt.Errorf("invalid protocol format: %s", s) 50 | } 51 | s = s[1 : len(s)-1] 52 | 53 | // Convert string to Protocol 54 | switch s { 55 | case "tcp": 56 | *p = TCP 57 | case "udp": 58 | *p = UDP 59 | case "arp": 60 | *p = ARP 61 | default: 62 | return fmt.Errorf("unknown protocol: %s", s) 63 | } 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/result/confidence/confidence.go: -------------------------------------------------------------------------------- 1 | package confidence 2 | 3 | type ConfidenceLevel uint8 4 | 5 | const ( 6 | Normal ConfidenceLevel = iota 7 | Low 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/result/results.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/projectdiscovery/naabu/v2/pkg/port" 7 | "github.com/projectdiscovery/naabu/v2/pkg/result/confidence" 8 | "golang.org/x/exp/maps" 9 | ) 10 | 11 | type ResultFn func(*HostResult) 12 | 13 | type HostResult struct { 14 | Host string 15 | IP string 16 | Ports []*port.Port 17 | Confidence confidence.ConfidenceLevel 18 | } 19 | 20 | // Result of the scan 21 | type Result struct { 22 | sync.RWMutex 23 | ipPorts map[string]map[string]*port.Port 24 | ips map[string]struct{} 25 | skipped map[string]struct{} 26 | } 27 | 28 | // NewResult structure 29 | func NewResult() *Result { 30 | ipPorts := make(map[string]map[string]*port.Port) 31 | ips := make(map[string]struct{}) 32 | skipped := make(map[string]struct{}) 33 | return &Result{ipPorts: ipPorts, ips: ips, skipped: skipped} 34 | } 35 | 36 | // AddPort to a specific ip 37 | func (r *Result) GetIPs() chan string { 38 | r.Lock() 39 | 40 | out := make(chan string) 41 | 42 | go func() { 43 | defer close(out) 44 | defer r.Unlock() 45 | 46 | for ip := range r.ips { 47 | out <- ip 48 | } 49 | }() 50 | 51 | return out 52 | } 53 | 54 | func (r *Result) HasIPS() bool { 55 | r.RLock() 56 | defer r.RUnlock() 57 | 58 | return len(r.ips) > 0 59 | } 60 | 61 | // GetIpsPorts returns the ips and ports 62 | func (r *Result) GetIPsPorts() chan *HostResult { 63 | r.RLock() 64 | 65 | out := make(chan *HostResult) 66 | 67 | go func() { 68 | defer close(out) 69 | defer r.RUnlock() 70 | 71 | for ip, ports := range r.ipPorts { 72 | confidenceLevel := confidence.Normal 73 | if r.HasSkipped(ip) { 74 | confidenceLevel = confidence.Low 75 | } 76 | out <- &HostResult{IP: ip, Ports: maps.Values(ports), Confidence: confidenceLevel} 77 | } 78 | }() 79 | 80 | return out 81 | } 82 | 83 | func (r *Result) HasIPsPorts() bool { 84 | r.RLock() 85 | defer r.RUnlock() 86 | 87 | return len(r.ipPorts) > 0 88 | } 89 | 90 | // AddPort to a specific ip 91 | func (r *Result) AddPort(ip string, p *port.Port) { 92 | r.Lock() 93 | defer r.Unlock() 94 | 95 | if _, ok := r.ipPorts[ip]; !ok { 96 | r.ipPorts[ip] = make(map[string]*port.Port) 97 | } 98 | 99 | r.ipPorts[ip][p.String()] = p 100 | r.ips[ip] = struct{}{} 101 | } 102 | 103 | // SetPorts for a specific ip 104 | func (r *Result) SetPorts(ip string, ports []*port.Port) { 105 | r.Lock() 106 | defer r.Unlock() 107 | 108 | if _, ok := r.ipPorts[ip]; !ok { 109 | r.ipPorts[ip] = make(map[string]*port.Port) 110 | } 111 | 112 | for _, p := range ports { 113 | r.ipPorts[ip][p.String()] = p 114 | } 115 | r.ips[ip] = struct{}{} 116 | } 117 | 118 | // IPHasPort checks if an ip has a specific port 119 | func (r *Result) IPHasPort(ip string, p *port.Port) bool { 120 | r.RLock() 121 | defer r.RUnlock() 122 | 123 | ipPorts, hasports := r.ipPorts[ip] 124 | if !hasports { 125 | return false 126 | } 127 | _, hasport := ipPorts[p.String()] 128 | 129 | return hasport 130 | } 131 | 132 | // AddIp adds an ip to the results 133 | func (r *Result) AddIp(ip string) { 134 | r.Lock() 135 | defer r.Unlock() 136 | 137 | r.ips[ip] = struct{}{} 138 | } 139 | 140 | // HasIP checks if an ip has been seen 141 | func (r *Result) HasIP(ip string) bool { 142 | r.RLock() 143 | defer r.RUnlock() 144 | 145 | _, ok := r.ips[ip] 146 | return ok 147 | } 148 | 149 | func (r *Result) IsEmpty() bool { 150 | return r.Len() == 0 151 | } 152 | 153 | func (r *Result) Len() int { 154 | r.RLock() 155 | defer r.RUnlock() 156 | 157 | return len(r.ips) 158 | } 159 | 160 | // GetPortCount returns the number of ports discovered for an ip 161 | func (r *Result) GetPortCount(host string) int { 162 | r.RLock() 163 | defer r.RUnlock() 164 | 165 | return len(r.ipPorts[host]) 166 | } 167 | 168 | // AddSkipped adds an ip to the skipped list 169 | func (r *Result) AddSkipped(ip string) { 170 | r.Lock() 171 | defer r.Unlock() 172 | 173 | r.skipped[ip] = struct{}{} 174 | } 175 | 176 | // HasSkipped checks if an ip has been skipped 177 | func (r *Result) HasSkipped(ip string) bool { 178 | r.RLock() 179 | defer r.RUnlock() 180 | 181 | _, ok := r.skipped[ip] 182 | return ok 183 | } 184 | -------------------------------------------------------------------------------- /pkg/result/results_test.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/projectdiscovery/naabu/v2/pkg/port" 7 | "github.com/projectdiscovery/naabu/v2/pkg/protocol" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestAddPort(t *testing.T) { 12 | targetIP := "127.0.0.1" 13 | targetPort := &port.Port{Port: 8080, Protocol: protocol.TCP} 14 | targetPorts := map[string]*port.Port{targetPort.String(): targetPort} 15 | 16 | res := NewResult() 17 | res.AddPort(targetIP, targetPort) 18 | 19 | expectedIPS := map[string]struct{}{targetIP: {}} 20 | assert.Equal(t, expectedIPS, res.ips) 21 | 22 | expectedIPSPorts := map[string]map[string]*port.Port{targetIP: targetPorts} 23 | assert.Equal(t, res.ipPorts, expectedIPSPorts) 24 | } 25 | 26 | func TestSetPorts(t *testing.T) { 27 | targetIP := "127.0.0.1" 28 | port80 := &port.Port{Port: 80, Protocol: protocol.TCP} 29 | port443 := &port.Port{Port: 443, Protocol: protocol.TCP} 30 | targetPorts := map[string]*port.Port{ 31 | port80.String(): port80, 32 | port443.String(): port443, 33 | } 34 | 35 | res := NewResult() 36 | res.SetPorts(targetIP, []*port.Port{port80, port443}) 37 | 38 | expectedIPS := map[string]struct{}{targetIP: {}} 39 | assert.Equal(t, res.ips, expectedIPS) 40 | 41 | expectedIPSPorts := map[string]map[string]*port.Port{targetIP: targetPorts} 42 | assert.Equal(t, res.ipPorts, expectedIPSPorts) 43 | } 44 | 45 | func TestIPHasPort(t *testing.T) { 46 | targetIP := "127.0.0.1" 47 | expectedPort := &port.Port{Port: 8080, Protocol: protocol.TCP} 48 | unexpectedPort := &port.Port{Port: 8081, Protocol: protocol.TCP} 49 | 50 | res := NewResult() 51 | res.AddPort(targetIP, expectedPort) 52 | assert.True(t, res.IPHasPort(targetIP, expectedPort)) 53 | assert.False(t, res.IPHasPort(targetIP, unexpectedPort)) 54 | } 55 | 56 | func TestAddIP(t *testing.T) { 57 | targetIP := "127.0.0.1" 58 | 59 | res := NewResult() 60 | res.AddIp(targetIP) 61 | expectedIPS := map[string]struct{}{targetIP: {}} 62 | assert.Equal(t, res.ips, expectedIPS) 63 | } 64 | 65 | func TestHasIP(t *testing.T) { 66 | targetIP := "127.0.0.1" 67 | 68 | res := NewResult() 69 | res.AddIp(targetIP) 70 | assert.True(t, res.HasIP(targetIP)) 71 | assert.False(t, res.HasIP("1.2.3.4")) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/routing/gateway_ip.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "os/exec" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | var ( 13 | GatewayMac = make(map[string]net.HardwareAddr) 14 | ) 15 | 16 | func GetGatewayMac(gateway string) (net.HardwareAddr, error) { 17 | if IP, ok := GatewayMac[gateway]; ok { 18 | return IP, nil 19 | } 20 | 21 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 22 | defer cancel() 23 | 24 | cmd := exec.CommandContext(ctx, "arp", "-n", gateway) 25 | output, err := cmd.CombinedOutput() 26 | if err != nil { 27 | return nil, err 28 | } 29 | for _, part := range strings.Fields(string(output)) { 30 | mc, err := net.ParseMAC(part) 31 | if err != nil { 32 | continue 33 | } 34 | 35 | GatewayMac[gateway] = mc 36 | return mc, nil 37 | } 38 | 39 | return nil, errors.New("gateway mac not found") 40 | } 41 | -------------------------------------------------------------------------------- /pkg/routing/router.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | iputil "github.com/projectdiscovery/utils/ip" 11 | ) 12 | 13 | type RouteType string 14 | 15 | const ( 16 | IPv4 RouteType = "IPv4" 17 | IPv6 RouteType = "IPv6" 18 | ) 19 | 20 | func (routeType RouteType) String() string { 21 | return strings.ToLower(string(routeType)) 22 | } 23 | 24 | type Route struct { 25 | Type RouteType 26 | Default bool 27 | NetworkInterface *net.Interface 28 | Destination string 29 | Gateway string 30 | Flags string 31 | Expire string 32 | DefaultSourceIP net.IP 33 | } 34 | 35 | // Router shares the same interface described in https://github.com/gopacket/gopacket 36 | type Router interface { 37 | // Route returns where to route a packet based on the packet's source 38 | // and destination IP address. 39 | // 40 | // Callers may pass in nil for src, in which case the src is treated as 41 | // either 0.0.0.0 or ::, depending on whether dst is a v4 or v6 address. 42 | // 43 | // It returns the interface on which to send the packet, the gateway IP 44 | // to send the packet to (if necessary), the preferred src IP to use (if 45 | // available). If the preferred src address is not given in the routing 46 | // table, the first IP address of the interface is provided. 47 | // 48 | // If an error is encountered, iface, geteway, and 49 | // preferredSrc will be nil, and err will be set. 50 | Route(dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) 51 | 52 | // RouteWithSrc routes based on source information as well as destination 53 | // information. Either or both of input/src can be nil. If both are, this 54 | // should behave exactly like Route(dst) 55 | RouteWithSrc(input net.HardwareAddr, src, dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) 56 | } 57 | 58 | func FindRouteForIp(ip net.IP, routes []*Route) (*Route, error) { 59 | var defaultRoute4, defaultRoute6 *Route 60 | // first we need to find the interface associated to the destination 61 | for _, route := range routes { 62 | if defaultRoute4 == nil && route.Default && route.Type == IPv4 { 63 | defaultRoute4 = route 64 | } 65 | if defaultRoute6 == nil && route.Default && route.Type == IPv6 { 66 | defaultRoute6 = route 67 | } 68 | // the destination can be an ip or cidr 69 | if itfDestIP := net.ParseIP(route.Destination); itfDestIP != nil { 70 | // if it's an ip compare it with our dest 71 | if itfDestIP.Equal(ip) { 72 | return route, nil 73 | } 74 | } 75 | // if it's a cidr, verify that the destination ip is contained 76 | if _, itfDrstCidr, err := net.ParseCIDR(route.Destination); err == nil { 77 | if itfDrstCidr.Contains(ip) { 78 | return route, nil 79 | } 80 | } 81 | } 82 | 83 | switch { 84 | case iputil.IsIPv4(ip) && defaultRoute4 != nil: 85 | return defaultRoute4, nil 86 | case iputil.IsIPv6(ip) && defaultRoute6 != nil: 87 | return defaultRoute6, nil 88 | } 89 | 90 | return nil, fmt.Errorf("route not found for %s", ip) 91 | } 92 | 93 | func FindSourceIpForIp(route *Route, ip net.IP) (net.IP, error) { 94 | addresses, err := route.NetworkInterface.Addrs() 95 | if err != nil { 96 | return nil, err 97 | } 98 | for _, address := range addresses { 99 | ipNet, ok := address.(*net.IPNet) 100 | if !ok || ipNet == nil { 101 | continue 102 | } 103 | ipAddress := ipNet.IP 104 | switch { 105 | case iputil.IsIPv4(ip, ipAddress): 106 | return ipAddress, nil 107 | case iputil.IsIPv6(ip, ipAddress) && !ipAddress.IsLinkLocalUnicast(): // link local unicast are not routeable 108 | return ipAddress, nil 109 | } 110 | } 111 | 112 | return nil, fmt.Errorf("could not find source ip for target \"%s\" with interface %s", ip, route.NetworkInterface.Name) 113 | } 114 | 115 | func GetOutboundIPs() (net.IP, net.IP, error) { 116 | // collect default outbound ipv4 and ipv6 117 | srcIPv4, err := iputil.GetSourceIP("128.199.158.128") // scanme.sh 118 | if err != nil { 119 | return nil, nil, errors.Wrap(err, "couldn't determine ipv4 routing interface") 120 | } 121 | 122 | // ignores errors on ipv6 routing 123 | srcIPv6, err := iputil.GetSourceIP("2400:6180:0:d0::91:1001") // scanme.sh 124 | if err != nil { 125 | return srcIPv4, nil, errors.Wrap(err, "couldn't determine ipv6 routing interface") 126 | } 127 | 128 | return srcIPv4, srcIPv6, nil 129 | } 130 | 131 | func FindRouteWithHwAndIp(hardwareAddr net.HardwareAddr, src net.IP, routes []*Route) (*Route, error) { 132 | for _, route := range routes { 133 | if bytes.EqualFold(route.NetworkInterface.HardwareAddr, hardwareAddr) { 134 | if src != nil { 135 | addresses, err := route.NetworkInterface.Addrs() 136 | if err != nil { 137 | return nil, err 138 | } 139 | for _, address := range addresses { 140 | if addressIP, ok := address.(*net.IPNet); ok { 141 | if addressIP.IP.Equal(src) { 142 | return route, nil 143 | } 144 | } 145 | } 146 | } else { 147 | return route, nil 148 | } 149 | } 150 | } 151 | 152 | return nil, errors.New("route not found") 153 | } 154 | 155 | func FindInterfaceByIp(ip net.IP) (*net.Interface, error) { 156 | interfaces, err := net.Interfaces() 157 | if err != nil { 158 | return nil, err 159 | } 160 | for _, itf := range interfaces { 161 | addresses, err := itf.Addrs() 162 | if err != nil { 163 | return nil, err 164 | } 165 | for _, address := range addresses { 166 | ipNet, ok := address.(*net.IPNet) 167 | if !ok || ipNet == nil { 168 | continue 169 | } 170 | ipAddress := ipNet.IP 171 | // check if they are equal 172 | areEqual := ipAddress.Equal(ip) 173 | if !areEqual { 174 | continue 175 | } 176 | // double check if they belongs to the same family as go standard library is faulty 177 | switch { 178 | case iputil.IsIPv4(ip, ipAddress): 179 | return &itf, nil 180 | case iputil.IsIPv6(ip, ipAddress): 181 | return &itf, nil 182 | } 183 | } 184 | } 185 | 186 | return nil, errors.New("interface not found") 187 | } 188 | -------------------------------------------------------------------------------- /pkg/routing/router_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package routing 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "fmt" 9 | "net" 10 | "os/exec" 11 | "strings" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/projectdiscovery/gologger" 15 | sliceutil "github.com/projectdiscovery/utils/slice" 16 | stringsutil "github.com/projectdiscovery/utils/strings" 17 | "go.uber.org/multierr" 18 | ) 19 | 20 | // New creates a routing engine for Darwin 21 | func New() (Router, error) { 22 | var routes []*Route 23 | netstatCmd := exec.Command("netstat", "-nr") 24 | netstatOutput, err := netstatCmd.Output() 25 | if err != nil { 26 | var route4, route6 *Route 27 | // create default routes with outgoing ips 28 | ip4, ip6, errOutboundIps := GetOutboundIPs() 29 | if ip4 != nil { 30 | interface4, err := FindInterfaceByIp(ip4) 31 | if err != nil { 32 | return nil, err 33 | } 34 | route4 = &Route{ 35 | Type: IPv4, 36 | Default: true, 37 | DefaultSourceIP: ip4, 38 | NetworkInterface: interface4, 39 | } 40 | routes = append(routes, route4) 41 | } 42 | 43 | // try to find outbound route for ipv6 44 | if ip6 != nil { 45 | interface6, _ := FindInterfaceByIp(ip6) 46 | route6 = &Route{ 47 | Type: IPv6, 48 | Default: true, 49 | DefaultSourceIP: ip6, 50 | NetworkInterface: interface6, 51 | } 52 | routes = append(routes, route6) 53 | } else { 54 | // if we fail, use the same network interface for ipv4 55 | route6 = &Route{ 56 | Type: IPv6, 57 | Default: true, 58 | NetworkInterface: route4.NetworkInterface, 59 | } 60 | routes = append(routes, route6) 61 | } 62 | if len(routes) > 0 { 63 | return &RouterDarwin{Routes: routes}, nil 64 | } 65 | return nil, multierr.Combine(err, errOutboundIps) 66 | } 67 | 68 | var lastType RouteType 69 | 70 | scanner := bufio.NewScanner(bytes.NewReader(netstatOutput)) 71 | for scanner.Scan() { 72 | outputLine := strings.TrimSpace(scanner.Text()) 73 | if outputLine == "" { 74 | continue 75 | } 76 | 77 | parts := stringsutil.SplitAny(outputLine, " \t") 78 | if len(parts) >= 4 && !sliceutil.Contains(parts, "Destination") { 79 | expire := "-1" 80 | if len(parts) > 4 { 81 | expire = parts[4] 82 | } 83 | 84 | route := &Route{ 85 | Default: stringsutil.EqualFoldAny(parts[0], "default"), 86 | Destination: parts[0], 87 | Gateway: parts[1], 88 | Flags: parts[2], 89 | Expire: expire, 90 | } 91 | 92 | if networkInterface, err := net.InterfaceByName(parts[3]); err == nil { 93 | route.NetworkInterface = networkInterface 94 | } 95 | 96 | hasDots := stringsutil.ContainsAny(route.Destination, ".") || stringsutil.ContainsAny(route.Gateway, ".") 97 | hasSemicolon := stringsutil.ContainsAny(route.Destination, ":") || stringsutil.ContainsAny(route.Gateway, ":") 98 | switch { 99 | case hasDots: 100 | route.Type = IPv4 101 | case hasSemicolon: 102 | route.Type = IPv6 103 | default: 104 | // use last route type and print a warning 105 | if lastType != "" { 106 | gologger.Debug().Msgf("using '%s' for unknown route type: '%s'\n", lastType, outputLine) 107 | route.Type = lastType 108 | } else { 109 | // we can't determine the route type 110 | return nil, fmt.Errorf("could not determine route type for: '%s'", outputLine) 111 | } 112 | } 113 | lastType = route.Type 114 | routes = append(routes, route) 115 | } 116 | } 117 | 118 | return &RouterDarwin{Routes: routes}, err 119 | } 120 | 121 | type RouterDarwin struct { 122 | Routes []*Route 123 | } 124 | 125 | func (r *RouterDarwin) Route(dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) { 126 | route, err := FindRouteForIp(dst, r.Routes) 127 | if err != nil { 128 | return nil, nil, nil, errors.Wrap(err, "could not find route") 129 | } 130 | 131 | if route.DefaultSourceIP != nil { 132 | return nil, nil, route.DefaultSourceIP, nil 133 | } 134 | 135 | if route.NetworkInterface == nil { 136 | return nil, nil, nil, errors.Wrap(err, "could not find network interface") 137 | } 138 | ip, err := FindSourceIpForIp(route, dst) 139 | if err != nil { 140 | return nil, nil, nil, errors.Wrap(err, "could not find source ip") 141 | } 142 | 143 | return route.NetworkInterface, net.ParseIP(route.Gateway), ip, nil 144 | } 145 | 146 | func (r *RouterDarwin) RouteWithSrc(input net.HardwareAddr, src, dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) { 147 | route, err := FindRouteWithHwAndIp(input, src, r.Routes) 148 | if err != nil { 149 | return nil, nil, nil, err 150 | } 151 | return route.NetworkInterface, net.ParseIP(route.Gateway), src, nil 152 | } 153 | -------------------------------------------------------------------------------- /pkg/routing/router_freebsd.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd 2 | 3 | package routing 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "fmt" 9 | "net" 10 | "os/exec" 11 | "strings" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/projectdiscovery/gologger" 15 | sliceutil "github.com/projectdiscovery/utils/slice" 16 | stringsutil "github.com/projectdiscovery/utils/strings" 17 | "go.uber.org/multierr" 18 | ) 19 | 20 | // New creates a routing engine for FreeBSD 21 | func New() (Router, error) { 22 | var routes []*Route 23 | netstatCmd := exec.Command("netstat", "-nr") 24 | netstatOutput, err := netstatCmd.Output() 25 | if err != nil { 26 | var route4, route6 *Route 27 | // create default routes with outgoing ips 28 | ip4, ip6, errOutboundIps := GetOutboundIPs() 29 | if ip4 != nil { 30 | interface4, err := FindInterfaceByIp(ip4) 31 | if err != nil { 32 | return nil, err 33 | } 34 | route4 = &Route{ 35 | Type: IPv4, 36 | Default: true, 37 | DefaultSourceIP: ip4, 38 | NetworkInterface: interface4, 39 | } 40 | routes = append(routes, route4) 41 | } 42 | 43 | // try to find outbound route for ipv6 44 | if ip6 != nil { 45 | interface6, _ := FindInterfaceByIp(ip6) 46 | route6 = &Route{ 47 | Type: IPv6, 48 | Default: true, 49 | DefaultSourceIP: ip6, 50 | NetworkInterface: interface6, 51 | } 52 | routes = append(routes, route6) 53 | } else { 54 | // if we fail, use the same network interface for ipv4 55 | route6 = &Route{ 56 | Type: IPv6, 57 | Default: true, 58 | NetworkInterface: route4.NetworkInterface, 59 | } 60 | routes = append(routes, route6) 61 | } 62 | if len(routes) > 0 { 63 | return &RouterFreebsd{Routes: routes}, nil 64 | } 65 | return nil, multierr.Combine(err, errOutboundIps) 66 | } 67 | 68 | var lastType RouteType 69 | 70 | scanner := bufio.NewScanner(bytes.NewReader(netstatOutput)) 71 | for scanner.Scan() { 72 | outputLine := strings.TrimSpace(scanner.Text()) 73 | if outputLine == "" { 74 | continue 75 | } 76 | 77 | parts := stringsutil.SplitAny(outputLine, " \t") 78 | if len(parts) >= 4 && !sliceutil.Contains(parts, "Destination") { 79 | expire := "-1" 80 | if len(parts) > 4 { 81 | expire = parts[4] 82 | } 83 | 84 | route := &Route{ 85 | Default: stringsutil.EqualFoldAny(parts[0], "default"), 86 | Destination: parts[0], 87 | Gateway: parts[1], 88 | Flags: parts[2], 89 | Expire: expire, 90 | } 91 | 92 | if networkInterface, err := net.InterfaceByName(parts[3]); err == nil { 93 | route.NetworkInterface = networkInterface 94 | } 95 | 96 | hasDots := stringsutil.ContainsAny(route.Destination, ".") || stringsutil.ContainsAny(route.Gateway, ".") 97 | hasSemicolon := stringsutil.ContainsAny(route.Destination, ":") || stringsutil.ContainsAny(route.Gateway, ":") 98 | switch { 99 | case hasDots: 100 | route.Type = IPv4 101 | case hasSemicolon: 102 | route.Type = IPv6 103 | default: 104 | // use last route type and print a warning 105 | if lastType != "" { 106 | gologger.Debug().Msgf("using '%s' for unknown route type: '%s'\n", lastType, outputLine) 107 | route.Type = lastType 108 | } else { 109 | // we can't determine the route type 110 | return nil, fmt.Errorf("could not determine route type for: '%s'", outputLine) 111 | } 112 | } 113 | lastType = route.Type 114 | routes = append(routes, route) 115 | } 116 | } 117 | 118 | return &RouterFreebsd{Routes: routes}, err 119 | } 120 | 121 | type RouterFreebsd struct { 122 | Routes []*Route 123 | } 124 | 125 | func (r *RouterFreebsd) Route(dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) { 126 | route, err := FindRouteForIp(dst, r.Routes) 127 | if err != nil { 128 | return nil, nil, nil, errors.Wrap(err, "could not find route") 129 | } 130 | 131 | if route.DefaultSourceIP != nil { 132 | return nil, nil, route.DefaultSourceIP, nil 133 | } 134 | 135 | if route.NetworkInterface == nil { 136 | return nil, nil, nil, errors.Wrap(err, "could not find network interface") 137 | } 138 | ip, err := FindSourceIpForIp(route, dst) 139 | if err != nil { 140 | return nil, nil, nil, errors.Wrap(err, "could not find source ip") 141 | } 142 | 143 | return route.NetworkInterface, net.ParseIP(route.Gateway), ip, nil 144 | } 145 | 146 | func (r *RouterFreebsd) RouteWithSrc(input net.HardwareAddr, src, dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) { 147 | route, err := FindRouteWithHwAndIp(input, src, r.Routes) 148 | if err != nil { 149 | return nil, nil, nil, err 150 | } 151 | return route.NetworkInterface, net.ParseIP(route.Gateway), src, nil 152 | } 153 | -------------------------------------------------------------------------------- /pkg/routing/router_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | // Copyright 2012 Google, Inc. All rights reserved. 4 | // 5 | // Use of this source code is governed by a BSD-style license 6 | // that can be found in the LICENSE file in the root of the source 7 | // tree. 8 | 9 | // Package routing provides a very basic but mostly functional implementation of 10 | // a routing table for IPv4/IPv6 addresses. It uses a routing table pulled from 11 | // the kernel via netlink to find the correct interface, gateway, and preferred 12 | // source IP address for packets destined to a particular location. 13 | // 14 | // The routing package is meant to be used with applications that are sending 15 | // raw packet data, which don't have the benefit of having the kernel route 16 | // packets for them. 17 | package routing 18 | 19 | import ( 20 | "bytes" 21 | "errors" 22 | "fmt" 23 | "net" 24 | "sort" 25 | "strings" 26 | "syscall" 27 | "unsafe" 28 | ) 29 | 30 | // Pulled from http://man7.org/linux/man-pages/man7/rtnetlink.7.html 31 | // See the section on RTM_NEWROUTE, specifically 'struct rtmsg'. 32 | type routeInfoInMemory struct { 33 | Family byte 34 | DstLen byte 35 | SrcLen byte 36 | TOS byte 37 | 38 | Table byte 39 | Protocol byte 40 | Scope byte 41 | Type byte 42 | 43 | Flags uint32 44 | } 45 | 46 | // rtInfo contains information on a single route. 47 | type rtInfo struct { 48 | Src, Dst *net.IPNet 49 | Gateway, PrefSrc net.IP 50 | // We currently ignore the InputIface. 51 | InputIface, OutputIface uint32 52 | Priority uint32 53 | } 54 | 55 | // routeSlice implements sort.Interface to sort routes by Priority. 56 | type routeSlice []*rtInfo 57 | 58 | func (r routeSlice) Len() int { 59 | return len(r) 60 | } 61 | func (r routeSlice) Less(i, j int) bool { 62 | return r[i].Priority < r[j].Priority 63 | } 64 | func (r routeSlice) Swap(i, j int) { 65 | r[i], r[j] = r[j], r[i] 66 | } 67 | 68 | type router struct { 69 | ifaces map[int]*net.Interface 70 | addrs map[int]ipAddrs 71 | v4, v6 routeSlice 72 | } 73 | 74 | func (r *router) String() string { 75 | strs := []string{"ROUTER", "--- V4 ---"} 76 | for _, route := range r.v4 { 77 | strs = append(strs, fmt.Sprintf("%+v", *route)) 78 | } 79 | strs = append(strs, "--- V6 ---") 80 | for _, route := range r.v6 { 81 | strs = append(strs, fmt.Sprintf("%+v", *route)) 82 | } 83 | return strings.Join(strs, "\n") 84 | } 85 | 86 | type ipAddrs struct { 87 | v4, v6 net.IP 88 | } 89 | 90 | func (r *router) Route(dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) { 91 | return r.RouteWithSrc(nil, nil, dst) 92 | } 93 | 94 | func (r *router) RouteWithSrc(input net.HardwareAddr, src, dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) { 95 | var ifaceIndex int 96 | switch { 97 | case dst.To4() != nil: 98 | ifaceIndex, gateway, preferredSrc, err = r.route(r.v4, input, src, dst) 99 | case dst.To16() != nil: 100 | ifaceIndex, gateway, preferredSrc, err = r.route(r.v6, input, src, dst) 101 | default: 102 | err = errors.New("IP is not valid as IPv4 or IPv6") 103 | } 104 | 105 | if err != nil { 106 | return 107 | } 108 | 109 | iface = r.ifaces[ifaceIndex] 110 | 111 | if preferredSrc == nil { 112 | switch { 113 | case dst.To4() != nil: 114 | preferredSrc = r.addrs[ifaceIndex].v4 115 | case dst.To16() != nil: 116 | preferredSrc = r.addrs[ifaceIndex].v6 117 | } 118 | } 119 | return 120 | } 121 | 122 | func (r *router) route(routes routeSlice, input net.HardwareAddr, src, dst net.IP) (iface int, gateway, preferredSrc net.IP, err error) { 123 | var inputIndex uint32 124 | if input != nil { 125 | for i, iface := range r.ifaces { 126 | if bytes.Equal(input, iface.HardwareAddr) { 127 | inputIndex = uint32(i) 128 | break 129 | } 130 | } 131 | } 132 | var defaultGateway *rtInfo = nil 133 | for _, rt := range routes { 134 | if rt.InputIface != 0 && rt.InputIface != inputIndex { 135 | continue 136 | } 137 | if rt.Src == nil && rt.Dst == nil { 138 | defaultGateway = rt 139 | continue 140 | } 141 | if rt.Src != nil && !rt.Src.Contains(src) { 142 | continue 143 | } 144 | if rt.Dst != nil && !rt.Dst.Contains(dst) { 145 | continue 146 | } 147 | return int(rt.OutputIface), rt.Gateway, rt.PrefSrc, nil 148 | } 149 | 150 | if defaultGateway != nil { 151 | return int(defaultGateway.OutputIface), defaultGateway.Gateway, defaultGateway.PrefSrc, nil 152 | } 153 | err = fmt.Errorf("no route found for %v", dst) 154 | return 155 | } 156 | 157 | // New creates a new router object. The router returned by New currently does 158 | // not update its routes after construction... care should be taken for 159 | // long-running programs to call New() regularly to take into account any 160 | // changes to the routing table which have occurred since the last New() call. 161 | func New() (Router, error) { 162 | rtr := &router{ 163 | ifaces: make(map[int]*net.Interface), 164 | addrs: make(map[int]ipAddrs), 165 | } 166 | tab, err := syscall.NetlinkRIB(syscall.RTM_GETROUTE, syscall.AF_UNSPEC) 167 | if err != nil { 168 | return nil, err 169 | } 170 | msgs, err := syscall.ParseNetlinkMessage(tab) 171 | if err != nil { 172 | return nil, err 173 | } 174 | loop: 175 | for _, m := range msgs { 176 | switch m.Header.Type { 177 | case syscall.NLMSG_DONE: 178 | break loop 179 | case syscall.RTM_NEWROUTE: 180 | rt := (*routeInfoInMemory)(unsafe.Pointer(&m.Data[0])) 181 | routeInfo := rtInfo{} 182 | attrs, err := syscall.ParseNetlinkRouteAttr(&m) 183 | if err != nil { 184 | return nil, err 185 | } 186 | switch rt.Family { 187 | case syscall.AF_INET: 188 | rtr.v4 = append(rtr.v4, &routeInfo) 189 | case syscall.AF_INET6: 190 | rtr.v6 = append(rtr.v6, &routeInfo) 191 | default: 192 | continue loop 193 | } 194 | for _, attr := range attrs { 195 | switch attr.Attr.Type { 196 | case syscall.RTA_DST: 197 | routeInfo.Dst = &net.IPNet{ 198 | IP: net.IP(attr.Value), 199 | Mask: net.CIDRMask(int(rt.DstLen), len(attr.Value)*8), 200 | } 201 | case syscall.RTA_SRC: 202 | routeInfo.Src = &net.IPNet{ 203 | IP: net.IP(attr.Value), 204 | Mask: net.CIDRMask(int(rt.SrcLen), len(attr.Value)*8), 205 | } 206 | case syscall.RTA_GATEWAY: 207 | routeInfo.Gateway = net.IP(attr.Value) 208 | case syscall.RTA_PREFSRC: 209 | routeInfo.PrefSrc = net.IP(attr.Value) 210 | case syscall.RTA_IIF: 211 | routeInfo.InputIface = *(*uint32)(unsafe.Pointer(&attr.Value[0])) 212 | case syscall.RTA_OIF: 213 | routeInfo.OutputIface = *(*uint32)(unsafe.Pointer(&attr.Value[0])) 214 | case syscall.RTA_PRIORITY: 215 | routeInfo.Priority = *(*uint32)(unsafe.Pointer(&attr.Value[0])) 216 | } 217 | } 218 | } 219 | } 220 | sort.Sort(rtr.v4) 221 | sort.Sort(rtr.v6) 222 | ifaces, err := net.Interfaces() 223 | if err != nil { 224 | return nil, err 225 | } 226 | for _, tmp := range ifaces { 227 | iface := tmp 228 | rtr.ifaces[iface.Index] = &iface 229 | var addrs ipAddrs 230 | ifaceAddrs, err := iface.Addrs() 231 | if err != nil { 232 | return nil, err 233 | } 234 | for _, addr := range ifaceAddrs { 235 | if inet, ok := addr.(*net.IPNet); ok { 236 | // Go has a nasty habit of giving you IPv4s as ::ffff:1.2.3.4 instead of 1.2.3.4. 237 | // We want to use mapped v4 addresses as v4 preferred addresses, never as v6 238 | // preferred addresses. 239 | if v4 := inet.IP.To4(); v4 != nil { 240 | if addrs.v4 == nil { 241 | addrs.v4 = v4 242 | } 243 | } else if addrs.v6 == nil { 244 | addrs.v6 = inet.IP 245 | } 246 | } 247 | } 248 | rtr.addrs[iface.Index] = addrs 249 | } 250 | return rtr, nil 251 | } 252 | -------------------------------------------------------------------------------- /pkg/routing/router_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package routing 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "net" 9 | "os/exec" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/asaskevich/govalidator" 14 | "github.com/pkg/errors" 15 | stringsutil "github.com/projectdiscovery/utils/strings" 16 | ) 17 | 18 | // New creates a routing engine for windows 19 | func New() (Router, error) { 20 | var routes []*Route 21 | 22 | for _, iptype := range []RouteType{IPv4, IPv6} { 23 | netshCmd := exec.Command("netsh", "interface", iptype.String(), "show", "route") 24 | netshOutput, err := netshCmd.Output() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | scanner := bufio.NewScanner(bytes.NewReader(netshOutput)) 30 | for scanner.Scan() { 31 | outputLine := strings.TrimSpace(scanner.Text()) 32 | if outputLine == "" { 33 | continue 34 | } 35 | 36 | parts := stringsutil.SplitAny(outputLine, " \t") 37 | if len(parts) >= 6 && govalidator.IsNumeric(parts[4]) { 38 | prefix := parts[3] 39 | _, _, err := net.ParseCIDR(prefix) 40 | if err != nil { 41 | return nil, err 42 | } 43 | gateway := parts[5] 44 | interfaceIndex, err := strconv.Atoi(parts[4]) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | networkInterface, err := net.InterfaceByIndex(interfaceIndex) 50 | if err != nil { 51 | return nil, err 52 | } 53 | isDefault := stringsutil.EqualFoldAny(prefix, "0.0.0.0/0", "::/0") 54 | 55 | route := &Route{ 56 | Type: iptype, 57 | Default: isDefault, 58 | Destination: prefix, 59 | Gateway: gateway, 60 | NetworkInterface: networkInterface, 61 | } 62 | 63 | routes = append(routes, route) 64 | } 65 | } 66 | } 67 | 68 | return &RouterWindows{Routes: routes}, nil 69 | } 70 | 71 | type RouterWindows struct { 72 | Routes []*Route 73 | } 74 | 75 | func (r *RouterWindows) Route(dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) { 76 | route, err := FindRouteForIp(dst, r.Routes) 77 | if err != nil { 78 | return nil, nil, nil, errors.Wrap(err, "could not find route") 79 | } 80 | 81 | if route.NetworkInterface == nil { 82 | return nil, nil, nil, errors.Wrap(err, "could not find network interface") 83 | } 84 | ip, err := FindSourceIpForIp(route, dst) 85 | if err != nil { 86 | return nil, nil, nil, errors.Wrap(err, "could not find source ip") 87 | } 88 | 89 | return route.NetworkInterface, net.IP(route.Gateway), ip, nil 90 | } 91 | 92 | func (r *RouterWindows) RouteWithSrc(input net.HardwareAddr, src, dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) { 93 | route, err := FindRouteWithHwAndIp(input, src, r.Routes) 94 | if err != nil { 95 | return nil, nil, nil, err 96 | } 97 | 98 | return route.NetworkInterface, net.IP(route.Gateway), src, nil 99 | } 100 | -------------------------------------------------------------------------------- /pkg/runner/banners.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | 7 | "github.com/projectdiscovery/gologger" 8 | "github.com/projectdiscovery/naabu/v2/pkg/privileges" 9 | "github.com/projectdiscovery/naabu/v2/pkg/scan" 10 | "github.com/projectdiscovery/utils/auth/pdcp" 11 | osutil "github.com/projectdiscovery/utils/os" 12 | updateutils "github.com/projectdiscovery/utils/update" 13 | ) 14 | 15 | const banner = ` 16 | __ 17 | ___ ___ ___ _/ / __ __ 18 | / _ \/ _ \/ _ \/ _ \/ // / 19 | /_//_/\_,_/\_,_/_.__/\_,_/ 20 | ` 21 | 22 | // Version is the current Version of naabu 23 | const Version = `2.3.4` 24 | 25 | // showBanner is used to show the banner to the user 26 | func showBanner() { 27 | gologger.Print().Msgf("%s\n", banner) 28 | gologger.Print().Msgf("\t\tprojectdiscovery.io\n\n") 29 | } 30 | 31 | // showNetworkCapabilities shows the network capabilities/scan types possible with the running user 32 | func showNetworkCapabilities(options *Options) { 33 | var accessLevel, scanType string 34 | 35 | switch { 36 | case privileges.IsPrivileged && options.ScanType == SynScan: 37 | accessLevel = "root" 38 | if osutil.IsLinux() { 39 | accessLevel = "CAP_NET_RAW" 40 | } 41 | scanType = "SYN" 42 | case options.Passive: 43 | accessLevel = "non root" 44 | scanType = "PASSIVE" 45 | default: 46 | accessLevel = "non root" 47 | scanType = "CONNECT" 48 | } 49 | 50 | switch { 51 | case options.OnlyHostDiscovery: 52 | scanType = "Host Discovery" 53 | gologger.Info().Msgf("Running %s\n", scanType) 54 | case options.Passive: 55 | scanType = "PASSIVE" 56 | gologger.Info().Msgf("Running %s scan\n", scanType) 57 | default: 58 | gologger.Info().Msgf("Running %s scan with %s privileges\n", scanType, accessLevel) 59 | } 60 | } 61 | 62 | func showHostDiscoveryInfo() { 63 | gologger.Info().Msgf("Running host discovery scan\n") 64 | } 65 | 66 | func showNetworkInterfaces() error { 67 | // Interfaces List 68 | interfaces, err := net.Interfaces() 69 | if err != nil { 70 | return err 71 | } 72 | for _, itf := range interfaces { 73 | addresses, addErr := itf.Addrs() 74 | if addErr != nil { 75 | gologger.Warning().Msgf("Could not retrieve addresses for %s: %s\n", itf.Name, addErr) 76 | continue 77 | } 78 | var addrstr []string 79 | for _, address := range addresses { 80 | addrstr = append(addrstr, address.String()) 81 | } 82 | gologger.Info().Msgf("Interface %s:\nMAC: %s\nAddresses: %s\nMTU: %d\nFlags: %s\n", itf.Name, itf.HardwareAddr, strings.Join(addrstr, " "), itf.MTU, itf.Flags.String()) 83 | } 84 | // External ip 85 | externalIP, err := scan.WhatsMyIP() 86 | if err != nil { 87 | gologger.Warning().Msgf("Could not obtain public ip: %s\n", err) 88 | } 89 | gologger.Info().Msgf("External Ip: %s\n", externalIP) 90 | 91 | return nil 92 | } 93 | 94 | // GetUpdateCallback returns a callback function that updates naabu 95 | func GetUpdateCallback() func() { 96 | return func() { 97 | showBanner() 98 | updateutils.GetUpdateToolCallback("naabu", Version)() 99 | } 100 | } 101 | 102 | // AuthWithPDCP is used to authenticate with PDCP 103 | func AuthWithPDCP() { 104 | showBanner() 105 | pdcp.CheckNValidateCredentials("naabu") 106 | } 107 | -------------------------------------------------------------------------------- /pkg/runner/banners_test.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestShowNetworkInterfaces(t *testing.T) { 10 | // non root users should be able to list interfaces 11 | assert.Nil(t, showNetworkInterfaces()) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/runner/default.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import "time" 4 | 5 | const ( 6 | DefaultPortTimeoutSynScan = time.Second 7 | DefaultPortTimeoutConnectScan = time.Duration(3 * time.Second) 8 | 9 | DefaultRateSynScan = 1000 10 | DefaultRateConnectScan = 1500 11 | 12 | DefaultRetriesSynScan = 3 13 | DefaultRetriesConnectScan = 3 14 | 15 | SynScan = "s" 16 | ConnectScan = "c" 17 | DefautStatsInterval = 5 18 | ) 19 | -------------------------------------------------------------------------------- /pkg/runner/healthcheck.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/projectdiscovery/goflags" 10 | "github.com/projectdiscovery/naabu/v2/pkg/privileges" 11 | fileutil "github.com/projectdiscovery/utils/file" 12 | ) 13 | 14 | func DoHealthCheck(options *Options, flagSet *goflags.FlagSet) string { 15 | // RW permissions on config file 16 | cfgFilePath, _ := flagSet.GetConfigFilePath() 17 | var test strings.Builder 18 | test.WriteString(fmt.Sprintf("Version: %s\n", Version)) 19 | test.WriteString(fmt.Sprintf("Operative System: %s\n", runtime.GOOS)) 20 | test.WriteString(fmt.Sprintf("Architecture: %s\n", runtime.GOARCH)) 21 | test.WriteString(fmt.Sprintf("Go Version: %s\n", runtime.Version())) 22 | test.WriteString(fmt.Sprintf("Compiler: %s\n", runtime.Compiler)) 23 | 24 | var testResult string 25 | if privileges.IsPrivileged { 26 | testResult = "Ok" 27 | } else { 28 | testResult = "Ko" 29 | } 30 | test.WriteString(fmt.Sprintf("Privileged/NET_RAW: %s\n", testResult)) 31 | 32 | ok, err := fileutil.IsReadable(cfgFilePath) 33 | if ok { 34 | testResult = "Ok" 35 | } else { 36 | testResult = "Ko" 37 | } 38 | if err != nil { 39 | testResult += fmt.Sprintf(" (%s)", err) 40 | } 41 | test.WriteString(fmt.Sprintf("Config file \"%s\" Read => %s\n", cfgFilePath, testResult)) 42 | ok, err = fileutil.IsWriteable(cfgFilePath) 43 | if ok { 44 | testResult = "Ok" 45 | } else { 46 | testResult = "Ko" 47 | } 48 | if err != nil { 49 | testResult += fmt.Sprintf(" (%s)", err) 50 | } 51 | test.WriteString(fmt.Sprintf("Config file \"%s\" Write => %s\n", cfgFilePath, testResult)) 52 | c4, err := net.Dial("tcp4", "scanme.sh:80") 53 | if err == nil && c4 != nil { 54 | _ = c4.Close() 55 | } 56 | testResult = "Ok" 57 | if err != nil { 58 | testResult = fmt.Sprintf("Ko (%s)", err) 59 | } 60 | test.WriteString(fmt.Sprintf("TCP IPv4 connectivity to scanme.sh:80 => %s\n", testResult)) 61 | c6, err := net.Dial("tcp6", "scanme.sh:80") 62 | if err == nil && c6 != nil { 63 | _ = c6.Close() 64 | } 65 | testResult = "Ok" 66 | if err != nil { 67 | testResult = fmt.Sprintf("Ko (%s)", err) 68 | } 69 | test.WriteString(fmt.Sprintf("TCP IPv6 connectivity to scanme.sh:80 => %s\n", testResult)) 70 | u4, err := net.Dial("udp4", "scanme.sh:53") 71 | if err == nil && u4 != nil { 72 | _ = u4.Close() 73 | } 74 | testResult = "Ok" 75 | if err != nil { 76 | testResult = fmt.Sprintf("Ko (%s)", err) 77 | } 78 | test.WriteString(fmt.Sprintf("UDP IPv4 connectivity to scanme.sh:80 => %s\n", testResult)) 79 | u6, err := net.Dial("udp6", "scanme.sh:80") 80 | if err == nil && u6 != nil { 81 | _ = u6.Close() 82 | } 83 | testResult = "Ok" 84 | if err != nil { 85 | testResult = fmt.Sprintf("Ko (%s)", err) 86 | } 87 | test.WriteString(fmt.Sprintf("UDP IPv6 connectivity to scanme.sh:80 => %s\n", testResult)) 88 | 89 | return test.String() 90 | } 91 | -------------------------------------------------------------------------------- /pkg/runner/ips.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "strings" 5 | 6 | fileutil "github.com/projectdiscovery/utils/file" 7 | iputil "github.com/projectdiscovery/utils/ip" 8 | ) 9 | 10 | func (r *Runner) parseExcludedIps(options *Options) ([]string, error) { 11 | var excludedIps []string 12 | if options.ExcludeIps != "" { 13 | for _, host := range strings.Split(options.ExcludeIps, ",") { 14 | ips, err := r.getExcludeItems(host) 15 | if err != nil { 16 | return nil, err 17 | } 18 | excludedIps = append(excludedIps, ips...) 19 | } 20 | } 21 | 22 | if options.ExcludeIpsFile != "" { 23 | cdata, err := fileutil.ReadFile(options.ExcludeIpsFile) 24 | if err != nil { 25 | return excludedIps, err 26 | } 27 | for host := range cdata { 28 | ips, err := r.getExcludeItems(host) 29 | if err != nil { 30 | return nil, err 31 | } 32 | excludedIps = append(excludedIps, ips...) 33 | } 34 | } 35 | 36 | return excludedIps, nil 37 | } 38 | 39 | func (r *Runner) getExcludeItems(s string) ([]string, error) { 40 | if isIpOrCidr(s) { 41 | return []string{s}, nil 42 | } 43 | 44 | ips4, ips6, err := r.host2ips(s) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return append(ips4, ips6...), nil 49 | } 50 | 51 | func isIpOrCidr(s string) bool { 52 | return iputil.IsIP(s) || iputil.IsCIDR(s) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/runner/ips_test.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | fileutil "github.com/projectdiscovery/utils/file" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestParseExcludedIps(t *testing.T) { 15 | tmpFileName, err := fileutil.GetTempFileName() 16 | require.Nil(t, err) 17 | expectedIpsFromCLI := []string{"8.8.8.0/24", "7.7.7.7"} 18 | expectedIpsFromFile := []string{"10.10.10.0/24", "192.168.1.0/24"} 19 | require.Nil(t, os.WriteFile(tmpFileName, []byte(strings.Join(expectedIpsFromFile, "\n")), 0755)) 20 | expected := append(expectedIpsFromCLI, expectedIpsFromFile...) 21 | 22 | r, err := NewRunner(&Options{}) 23 | require.Nil(t, err) 24 | 25 | actual, err := r.parseExcludedIps(&Options{ 26 | ExcludeIps: strings.Join(expectedIpsFromCLI, ","), 27 | ExcludeIpsFile: tmpFileName, 28 | }) 29 | require.Nil(t, err) 30 | require.Equal(t, expected, actual) 31 | 32 | defer func() { 33 | if err := os.RemoveAll(tmpFileName); err != nil { 34 | log.Printf("could not remove test file: %s\n", err) 35 | } 36 | }() 37 | } 38 | 39 | func TestIsIpOrCidr(t *testing.T) { 40 | valid := []string{"1.1.1.1", "2.2.2.2", "1.1.1.0/24"} 41 | invalid := []string{"1.1.1.1.1", "a.a.a.a", "77"} 42 | for _, validItem := range valid { 43 | require.True(t, isIpOrCidr(validItem)) 44 | } 45 | for _, invalidItem := range invalid { 46 | require.False(t, isIpOrCidr(invalidItem)) 47 | } 48 | } 49 | 50 | // Helper function for the following 3 tests 51 | func testIps(testIps []string) func() ([]*net.IPNet, []string) { 52 | ips := []*net.IPNet{} 53 | 54 | for _, ip := range testIps { 55 | _, net, _ := net.ParseCIDR(ip) 56 | ips = append(ips, net) 57 | } 58 | 59 | return func() ([]*net.IPNet, []string) { 60 | return ips, []string{} 61 | } 62 | } 63 | 64 | func TestIpV4Only(t *testing.T) { 65 | ips := []string{"1.1.1.1/32", "2.2.2.2/32", "1.1.1.0/24", "fe80::623e:5fff:fe76:7d82/64", "100.121.237.116/32", "fd7a:115c:a1e0::fb01:ed74/48"} 66 | 67 | r, err := NewRunner(&Options{ 68 | IPVersion: []string{"4"}, 69 | }) 70 | require.Nil(t, err) 71 | 72 | targets, targetsV4, targetsV6, _, err := r.GetTargetIps(testIps(ips)) 73 | require.Nil(t, err) 74 | require.Equal(t, targets, targetsV4) 75 | require.Empty(t, targetsV6) 76 | } 77 | 78 | func TestIpV6Only(t *testing.T) { 79 | ips := []string{"1.1.1.1/32", "2.2.2.2/32", "1.1.1.0/24", "fe80::623e:5fff:fe76:7d82/64", "100.121.237.116/32", "fd7a:115c:a1e0::fb01:ed74/48"} 80 | 81 | r, err := NewRunner(&Options{ 82 | IPVersion: []string{"6"}, 83 | }) 84 | require.Nil(t, err) 85 | 86 | targets, targetsV4, targetsV6, _, err := r.GetTargetIps(testIps(ips)) 87 | require.Nil(t, err) 88 | require.Equal(t, targets, targetsV6) 89 | require.Empty(t, targetsV4) 90 | } 91 | 92 | func TestIpV4AndV6(t *testing.T) { 93 | ips := []string{"1.1.1.1/32", "2.2.2.2/32", "1.1.1.0/24", "fe80::623e:5fff:fe76:7d82/64", "100.121.237.116/32", "fd7a:115c:a1e0::fb01:ed74/48"} 94 | 95 | r, err := NewRunner(&Options{ 96 | IPVersion: []string{"4", "6"}, 97 | }) 98 | require.Nil(t, err) 99 | 100 | targets, targetsV4, targetsV6, _, err := r.GetTargetIps(testIps(ips)) 101 | expected := append(targetsV4, targetsV6...) 102 | 103 | require.Nil(t, err) 104 | require.EqualValues(t, expected, targets) 105 | } 106 | -------------------------------------------------------------------------------- /pkg/runner/nmap.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | 12 | "github.com/projectdiscovery/gologger" 13 | "github.com/projectdiscovery/naabu/v2/pkg/result" 14 | osutil "github.com/projectdiscovery/utils/os" 15 | ) 16 | 17 | func (r *Runner) handleNmap() error { 18 | // command from CLI 19 | command := r.options.NmapCLI 20 | hasCLI := r.options.NmapCLI != "" 21 | if hasCLI { 22 | var ipsPorts []*result.HostResult 23 | // build a list of all targets 24 | for hostResult := range r.scanner.ScanResults.GetIPsPorts() { 25 | ipsPorts = append(ipsPorts, hostResult) 26 | } 27 | 28 | // sort by number of ports 29 | sort.Slice(ipsPorts, func(i, j int) bool { 30 | return len(ipsPorts[i].Ports) < len(ipsPorts[j].Ports) 31 | }) 32 | 33 | // suggests commands grouping ips in pseudo-exp ranges 34 | // 0 - 100 ports 35 | // 100 - 1000 ports 36 | // 1000 - 10000 ports 37 | // 10000 - 60000 ports 38 | ranges := make(map[int][]*result.HostResult) // for better readability 39 | // collect the indexes corresponding to ranges changes 40 | for _, ipPorts := range ipsPorts { 41 | length := len(ipPorts.Ports) 42 | var index int 43 | switch { 44 | case length > 100 && length < 1000: 45 | index = 1 46 | case length >= 1000 && length < 10000: 47 | index = 2 48 | case length >= 10000: 49 | index = 3 50 | default: 51 | index = 0 52 | } 53 | ranges[index] = append(ranges[index], ipPorts) 54 | } 55 | 56 | for _, rang := range ranges { 57 | args := strings.Split(command, " ") 58 | var ( 59 | ips []string 60 | ports []string 61 | ) 62 | allports := make(map[int]struct{}) 63 | for _, ipPorts := range rang { 64 | ips = append(ips, ipPorts.IP) 65 | for _, pp := range ipPorts.Ports { 66 | allports[pp.Port] = struct{}{} 67 | } 68 | } 69 | for p := range allports { 70 | ports = append(ports, fmt.Sprint(p)) 71 | } 72 | 73 | // if we have no open ports we avoid running nmap 74 | if len(ports) == 0 { 75 | continue 76 | } 77 | 78 | portsStr := strings.Join(ports, ",") 79 | ipsStr := strings.Join(ips, " ") 80 | 81 | args = append(args, "-p", portsStr) 82 | args = append(args, ips...) 83 | 84 | // if the command is not executable, we just suggest it 85 | commandCanBeExecuted := isCommandExecutable(args) 86 | 87 | // if requested via config file or via cli 88 | if (r.options.Nmap || hasCLI) && commandCanBeExecuted { 89 | gologger.Info().Msgf("Running nmap command: %s -p %s %s", command, portsStr, ipsStr) 90 | // check when user type '-nmap-cli "nmap -sV"' 91 | // automatically remove nmap 92 | posArgs := 0 93 | // nmapCommand helps to check if user is on a Windows machine 94 | nmapCommand := "nmap" 95 | if args[0] == "nmap" || args[0] == "nmap.exe" { 96 | posArgs = 1 97 | } 98 | 99 | // if it's windows search for the executable 100 | if osutil.IsWindows() { 101 | nmapCommand = "nmap.exe" 102 | } 103 | 104 | cmd := exec.Command(nmapCommand, args[posArgs:]...) 105 | 106 | cmd.Stdout = os.Stdout 107 | err := cmd.Run() 108 | if err != nil { 109 | errMsg := errors.Wrap(err, "Could not run nmap command") 110 | gologger.Error().Msg(errMsg.Error()) 111 | return errMsg 112 | } 113 | } else { 114 | gologger.Info().Msgf("Suggested nmap command: %s -p %s %s", command, portsStr, ipsStr) 115 | } 116 | } 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func isCommandExecutable(args []string) bool { 123 | commandLength := calculateCmdLength(args) 124 | if osutil.IsWindows() { 125 | // windows has a hard limit of 126 | // - 2048 characters in XP 127 | // - 32768 characters in Win7 128 | return commandLength < 2048 129 | } 130 | // linux and darwin 131 | return true 132 | } 133 | 134 | func calculateCmdLength(args []string) int { 135 | var commandLength int 136 | for _, arg := range args { 137 | commandLength += len(arg) 138 | commandLength += 1 // space character 139 | } 140 | return commandLength 141 | } 142 | -------------------------------------------------------------------------------- /pkg/runner/nmap_test.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/projectdiscovery/naabu/v2/pkg/port" 7 | "github.com/projectdiscovery/naabu/v2/pkg/protocol" 8 | "github.com/projectdiscovery/naabu/v2/pkg/result" 9 | "github.com/projectdiscovery/naabu/v2/pkg/scan" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestHandleNmap(t *testing.T) { 14 | // just attempt to start nmap 15 | var r Runner 16 | r.options = &Options{} 17 | // nmap with empty cli shouldn't trigger any error 18 | res := result.NewResult() 19 | r.scanner = &scan.Scanner{} 20 | r.scanner.ScanResults = res 21 | assert.Nil(t, r.handleNmap()) 22 | // nmap syntax error (this test might fail if nmap is not installed on the box) 23 | assert.Nil(t, r.handleNmap()) 24 | r.scanner.ScanResults.SetPorts("127.0.0.1", []*port.Port{{Port: 8080, Protocol: protocol.TCP}}) 25 | assert.Nil(t, r.handleNmap()) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/runner/output.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "bufio" 5 | "encoding/csv" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "reflect" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "golang.org/x/exp/slices" 15 | 16 | "github.com/pkg/errors" 17 | "github.com/projectdiscovery/gologger" 18 | "github.com/projectdiscovery/naabu/v2/pkg/port" 19 | "github.com/projectdiscovery/naabu/v2/pkg/protocol" 20 | "github.com/projectdiscovery/utils/structs" 21 | ) 22 | 23 | // Result contains the result for a host 24 | type Result struct { 25 | Host string `json:"host,omitempty" csv:"host"` 26 | IP string `json:"ip,omitempty" csv:"ip"` 27 | Port int `json:"port,omitempty" csv:"port"` 28 | Protocol string `json:"protocol,omitempty" csv:"protocol"` 29 | TLS bool `json:"tls,omitempty" csv:"tls"` 30 | IsCDNIP bool `json:"cdn,omitempty" csv:"cdn"` 31 | CDNName string `json:"cdn-name,omitempty" csv:"cdn-name"` 32 | TimeStamp time.Time `json:"timestamp,omitempty" csv:"timestamp"` 33 | } 34 | 35 | type jsonResult struct { 36 | Host string `json:"host,omitempty" csv:"host"` 37 | IP string `json:"ip,omitempty" csv:"ip"` 38 | IsCDNIP bool `json:"cdn,omitempty" csv:"cdn"` 39 | CDNName string `json:"cdn-name,omitempty" csv:"cdn-name"` 40 | TimeStamp time.Time `json:"timestamp,omitempty" csv:"timestamp"` 41 | PortNumber int `json:"port"` 42 | Protocol string `json:"protocol"` 43 | TLS bool `json:"tls"` 44 | } 45 | 46 | func (r *Result) JSON(excludedFields []string) ([]byte, error) { 47 | data := jsonResult{} 48 | data.TimeStamp = r.TimeStamp 49 | if r.Host != r.IP { 50 | data.Host = r.Host 51 | } 52 | data.IP = r.IP 53 | data.IsCDNIP = r.IsCDNIP 54 | data.CDNName = r.CDNName 55 | data.PortNumber = r.Port 56 | data.Protocol = r.Protocol 57 | data.TLS = r.TLS 58 | 59 | if len(excludedFields) > 0 { 60 | if filteredData, err := structs.FilterStruct(data, nil, excludedFields); err == nil { 61 | data = filteredData 62 | } 63 | } 64 | return json.Marshal(data) 65 | } 66 | 67 | var ( 68 | NumberOfCsvFieldsErr = errors.New("exported fields don't match csv tags") 69 | headers = []string{} 70 | ) 71 | 72 | func (r *Result) CSVHeaders(excludedFields []string) ([]string, error) { 73 | ty := reflect.TypeOf(*r) 74 | for i := 0; i < ty.NumField(); i++ { 75 | field := ty.Field(i) 76 | csvTag := field.Tag.Get("csv") 77 | if !slices.Contains(headers, csvTag) && !slices.Contains(excludedFields, csvTag) { 78 | headers = append(headers, csvTag) 79 | } 80 | } 81 | return headers, nil 82 | } 83 | 84 | func (r *Result) CSVFields(excludedFields []string) ([]string, error) { 85 | data := *r 86 | if len(excludedFields) > 0 { 87 | if filteredData, err := structs.FilterStruct(data, nil, excludedFields); err == nil { 88 | data = filteredData 89 | } 90 | } 91 | 92 | var fields []string 93 | vl := reflect.ValueOf(data) 94 | ty := reflect.TypeOf(data) 95 | for i := 0; i < vl.NumField(); i++ { 96 | field := vl.Field(i) 97 | csvTag := ty.Field(i).Tag.Get("csv") 98 | fieldValue := field.Interface() 99 | if slices.Contains(headers, csvTag) { 100 | fields = append(fields, fmt.Sprint(fieldValue)) 101 | } 102 | } 103 | return fields, nil 104 | } 105 | 106 | // WriteHostOutput writes the output list of host ports to an io.Writer 107 | func WriteHostOutput(host string, ports []*port.Port, outputCDN bool, cdnName string, writer io.Writer) error { 108 | bufwriter := bufio.NewWriter(writer) 109 | sb := &strings.Builder{} 110 | 111 | for _, p := range ports { 112 | sb.WriteString(host) 113 | sb.WriteString(":") 114 | sb.WriteString(strconv.Itoa(p.Port)) 115 | if outputCDN && cdnName != "" { 116 | sb.WriteString(" [" + cdnName + "]") 117 | } 118 | sb.WriteString("\n") 119 | _, err := bufwriter.WriteString(sb.String()) 120 | if err != nil { 121 | _ = bufwriter.Flush() 122 | return err 123 | } 124 | sb.Reset() 125 | } 126 | return bufwriter.Flush() 127 | } 128 | 129 | // WriteJSONOutput writes the output list of subdomain in JSON to an io.Writer 130 | func WriteJSONOutput(host, ip string, ports []*port.Port, outputCDN bool, isCdn bool, cdnName string, writer io.Writer) error { 131 | encoder := json.NewEncoder(writer) 132 | data := jsonResult{} 133 | data.TimeStamp = time.Now().UTC() 134 | if host != ip { 135 | data.Host = host 136 | } 137 | data.IP = ip 138 | if outputCDN { 139 | data.IsCDNIP = isCdn 140 | data.CDNName = cdnName 141 | } 142 | for _, p := range ports { 143 | data.PortNumber = p.Port 144 | data.Protocol = p.Protocol.String() 145 | //nolint 146 | data.TLS = p.TLS 147 | if err := encoder.Encode(&data); err != nil { 148 | return err 149 | } 150 | } 151 | return nil 152 | } 153 | 154 | // WriteCsvOutput writes the output list of subdomain in csv format to an io.Writer 155 | func WriteCsvOutput(host, ip string, ports []*port.Port, outputCDN bool, isCdn bool, cdnName string, header bool, excludedFields []string, writer io.Writer) error { 156 | encoder := csv.NewWriter(writer) 157 | data := &Result{IP: ip, TimeStamp: time.Now().UTC(), Port: 0, Protocol: protocol.TCP.String(), TLS: false} 158 | if host != ip { 159 | data.Host = host 160 | } 161 | if outputCDN { 162 | data.IsCDNIP = isCdn 163 | data.CDNName = cdnName 164 | } 165 | if header { 166 | writeCSVHeaders(data, encoder, excludedFields) 167 | } 168 | 169 | for _, p := range ports { 170 | data.Port = p.Port 171 | data.Protocol = p.Protocol.String() 172 | //nolint 173 | data.TLS = p.TLS 174 | writeCSVRow(data, encoder, excludedFields) 175 | } 176 | encoder.Flush() 177 | return nil 178 | } 179 | 180 | func writeCSVHeaders(data *Result, writer *csv.Writer, excludedFields []string) { 181 | headers, err := data.CSVHeaders(excludedFields) 182 | if err != nil { 183 | gologger.Error().Msg(err.Error()) 184 | return 185 | } 186 | 187 | if err := writer.Write(headers); err != nil { 188 | errMsg := errors.Wrap(err, "Could not write headers") 189 | gologger.Error().Msg(errMsg.Error()) 190 | } 191 | } 192 | 193 | func writeCSVRow(data *Result, writer *csv.Writer, excludedFields []string) { 194 | rowData, err := data.CSVFields(excludedFields) 195 | if err != nil { 196 | gologger.Error().Msg(err.Error()) 197 | return 198 | } 199 | if err := writer.Write(rowData); err != nil { 200 | errMsg := errors.Wrap(err, "Could not write row") 201 | gologger.Error().Msg(errMsg.Error()) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /pkg/runner/output_test.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/projectdiscovery/naabu/v2/pkg/port" 9 | "github.com/projectdiscovery/naabu/v2/pkg/protocol" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestWriteHostOutput(t *testing.T) { 14 | host := "127.0.0.1" 15 | ports := []*port.Port{ 16 | {Port: 80, Protocol: protocol.TCP}, 17 | {Port: 8080, Protocol: protocol.TCP}, 18 | } 19 | var s string 20 | buf := bytes.NewBufferString(s) 21 | assert.Nil(t, WriteHostOutput(host, ports, false, "", buf)) 22 | assert.Contains(t, buf.String(), "127.0.0.1:80") 23 | assert.Contains(t, buf.String(), "127.0.0.1:8080") 24 | } 25 | 26 | func TestWriteJSONOutput(t *testing.T) { 27 | host := "localhost" 28 | ip := "127.0.0.1" 29 | ports := []*port.Port{ 30 | {Port: 80, Protocol: protocol.TCP}, 31 | {Port: 8080, Protocol: protocol.TCP}, 32 | } 33 | var s string 34 | buf := bytes.NewBufferString(s) 35 | assert.Nil(t, WriteJSONOutput(host, ip, ports, true, false, "", buf)) 36 | assert.Equal(t, 3, len(strings.Split(buf.String(), "\n"))) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/runner/ports.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/projectdiscovery/naabu/v2/pkg/port" 11 | "github.com/projectdiscovery/naabu/v2/pkg/protocol" 12 | ) 13 | 14 | const portListStrParts = 2 15 | 16 | // List of default ports 17 | const ( 18 | Full = "1-65535" 19 | NmapTop100 = "7,9,13,21-23,25-26,37,53,79-81,88,106,110-111,113,119,135,139,143-144,179,199,389,427,443-445,465,513-515,543-544,548,554,587,631,646,873,990,993,995,1025-1029,1110,1433,1720,1723,1755,1900,2000-2001,2049,2121,2717,3000,3128,3306,3389,3986,4899,5000,5009,5051,5060,5101,5190,5357,5432,5631,5666,5800,5900,6000-6001,6646,7070,8000,8008-8009,8080-8081,8443,8888,9100,9999-10000,32768,49152-49157" 20 | NmapTop1000 = "1,3-4,6-7,9,13,17,19-26,30,32-33,37,42-43,49,53,70,79-85,88-90,99-100,106,109-111,113,119,125,135,139,143-144,146,161,163,179,199,211-212,222,254-256,259,264,280,301,306,311,340,366,389,406-407,416-417,425,427,443-445,458,464-465,481,497,500,512-515,524,541,543-545,548,554-555,563,587,593,616-617,625,631,636,646,648,666-668,683,687,691,700,705,711,714,720,722,726,749,765,777,783,787,800-801,808,843,873,880,888,898,900-903,911-912,981,987,990,992-993,995,999-1002,1007,1009-1011,1021-1100,1102,1104-1108,1110-1114,1117,1119,1121-1124,1126,1130-1132,1137-1138,1141,1145,1147-1149,1151-1152,1154,1163-1166,1169,1174-1175,1183,1185-1187,1192,1198-1199,1201,1213,1216-1218,1233-1234,1236,1244,1247-1248,1259,1271-1272,1277,1287,1296,1300-1301,1309-1311,1322,1328,1334,1352,1417,1433-1434,1443,1455,1461,1494,1500-1501,1503,1521,1524,1533,1556,1580,1583,1594,1600,1641,1658,1666,1687-1688,1700,1717-1721,1723,1755,1761,1782-1783,1801,1805,1812,1839-1840,1862-1864,1875,1900,1914,1935,1947,1971-1972,1974,1984,1998-2010,2013,2020-2022,2030,2033-2035,2038,2040-2043,2045-2049,2065,2068,2099-2100,2103,2105-2107,2111,2119,2121,2126,2135,2144,2160-2161,2170,2179,2190-2191,2196,2200,2222,2251,2260,2288,2301,2323,2366,2381-2383,2393-2394,2399,2401,2492,2500,2522,2525,2557,2601-2602,2604-2605,2607-2608,2638,2701-2702,2710,2717-2718,2725,2800,2809,2811,2869,2875,2909-2910,2920,2967-2968,2998,3000-3001,3003,3005-3007,3011,3013,3017,3030-3031,3052,3071,3077,3128,3168,3211,3221,3260-3261,3268-3269,3283,3300-3301,3306,3322-3325,3333,3351,3367,3369-3372,3389-3390,3404,3476,3493,3517,3527,3546,3551,3580,3659,3689-3690,3703,3737,3766,3784,3800-3801,3809,3814,3826-3828,3851,3869,3871,3878,3880,3889,3905,3914,3918,3920,3945,3971,3986,3995,3998,4000-4006,4045,4111,4125-4126,4129,4224,4242,4279,4321,4343,4443-4446,4449,4550,4567,4662,4848,4899-4900,4998,5000-5004,5009,5030,5033,5050-5051,5054,5060-5061,5080,5087,5100-5102,5120,5190,5200,5214,5221-5222,5225-5226,5269,5280,5298,5357,5405,5414,5431-5432,5440,5500,5510,5544,5550,5555,5560,5566,5631,5633,5666,5678-5679,5718,5730,5800-5802,5810-5811,5815,5822,5825,5850,5859,5862,5877,5900-5904,5906-5907,5910-5911,5915,5922,5925,5950,5952,5959-5963,5987-5989,5998-6007,6009,6025,6059,6100-6101,6106,6112,6123,6129,6156,6346,6389,6502,6510,6543,6547,6565-6567,6580,6646,6666-6669,6689,6692,6699,6779,6788-6789,6792,6839,6881,6901,6969,7000-7002,7004,7007,7019,7025,7070,7100,7103,7106,7200-7201,7402,7435,7443,7496,7512,7625,7627,7676,7741,7777-7778,7800,7911,7920-7921,7937-7938,7999-8002,8007-8011,8021-8022,8031,8042,8045,8080-8090,8093,8099-8100,8180-8181,8192-8194,8200,8222,8254,8290-8292,8300,8333,8383,8400,8402,8443,8500,8600,8649,8651-8652,8654,8701,8800,8873,8888,8899,8994,9000-9003,9009-9011,9040,9050,9071,9080-9081,9090-9091,9099-9103,9110-9111,9200,9207,9220,9290,9415,9418,9485,9500,9502-9503,9535,9575,9593-9595,9618,9666,9876-9878,9898,9900,9917,9929,9943-9944,9968,9998-10004,10009-10010,10012,10024-10025,10082,10180,10215,10243,10566,10616-10617,10621,10626,10628-10629,10778,11110-11111,11967,12000,12174,12265,12345,13456,13722,13782-13783,14000,14238,14441-14442,15000,15002-15004,15660,15742,16000-16001,16012,16016,16018,16080,16113,16992-16993,17877,17988,18040,18101,18988,19101,19283,19315,19350,19780,19801,19842,20000,20005,20031,20221-20222,20828,21571,22939,23502,24444,24800,25734-25735,26214,27000,27352-27353,27355-27356,27715,28201,30000,30718,30951,31038,31337,32768-32785,33354,33899,34571-34573,35500,38292,40193,40911,41511,42510,44176,44442-44443,44501,45100,48080,49152-49161,49163,49165,49167,49175-49176,49400,49999-50003,50006,50300,50389,50500,50636,50800,51103,51493,52673,52822,52848,52869,54045,54328,55055-55056,55555,55600,56737-56738,57294,57797,58080,60020,60443,61532,61900,62078,63331,64623,64680,65000,65129,65389" 21 | ) 22 | 23 | // ParsePorts parses the list of ports and creates a port map 24 | func ParsePorts(options *Options) ([]*port.Port, error) { 25 | var portsFileMap, portsCLIMap, topPortsCLIMap, portsConfigList []*port.Port 26 | 27 | // If the user has specfied a ports file, use it 28 | if options.PortsFile != "" { 29 | data, err := os.ReadFile(options.PortsFile) 30 | if err != nil { 31 | return nil, fmt.Errorf("could not read ports: %s", err) 32 | } 33 | ports, err := parsePortsList(string(data)) 34 | if err != nil { 35 | return nil, fmt.Errorf("could not read ports: %s", err) 36 | } 37 | portsFileMap, err = excludePorts(options, ports) 38 | if err != nil { 39 | return nil, fmt.Errorf("could not read ports: %s", err) 40 | } 41 | } 42 | 43 | // If the user has specfied top ports, use them as well 44 | if options.TopPorts != "" { 45 | switch strings.ToLower(options.TopPorts) { 46 | case "full": // If the user has specfied full ports, use them 47 | var err error 48 | ports, err := parsePortsList(Full) 49 | if err != nil { 50 | return nil, fmt.Errorf("could not read ports: %s", err) 51 | } 52 | topPortsCLIMap, err = excludePorts(options, ports) 53 | if err != nil { 54 | return nil, fmt.Errorf("could not read ports: %s", err) 55 | } 56 | case "100": // If the user has specfied 100, use them 57 | ports, err := parsePortsList(NmapTop100) 58 | if err != nil { 59 | return nil, fmt.Errorf("could not read ports: %s", err) 60 | } 61 | topPortsCLIMap, err = excludePorts(options, ports) 62 | if err != nil { 63 | return nil, fmt.Errorf("could not read ports: %s", err) 64 | } 65 | case "1000": // If the user has specfied 1000, use them 66 | ports, err := parsePortsList(NmapTop1000) 67 | if err != nil { 68 | return nil, fmt.Errorf("could not read ports: %s", err) 69 | } 70 | topPortsCLIMap, err = excludePorts(options, ports) 71 | if err != nil { 72 | return nil, fmt.Errorf("could not read ports: %s", err) 73 | } 74 | default: 75 | return nil, errors.New("invalid top ports option") 76 | } 77 | } 78 | 79 | // If the user has specfied ports option, use them too 80 | if options.Ports != "" { 81 | // "-" equals to all ports 82 | if options.Ports == "-" { 83 | // Parse the custom ports list provided by the user 84 | options.Ports = "1-65535" 85 | } 86 | ports, err := parsePortsList(options.Ports) 87 | if err != nil { 88 | return nil, fmt.Errorf("could not read ports: %s", err) 89 | } 90 | portsCLIMap, err = excludePorts(options, ports) 91 | if err != nil { 92 | return nil, fmt.Errorf("could not read ports: %s", err) 93 | } 94 | } 95 | 96 | // merge all the specified ports (meaningless if "all" is used) 97 | ports := merge(portsFileMap, portsCLIMap, topPortsCLIMap, portsConfigList) 98 | 99 | // By default scan top 100 ports only 100 | if len(ports) == 0 { 101 | portsList, err := parsePortsList(NmapTop100) 102 | if err != nil { 103 | return nil, fmt.Errorf("could not read ports: %s", err) 104 | } 105 | m, err := excludePorts(options, portsList) 106 | if err != nil { 107 | return nil, err 108 | } 109 | return m, nil 110 | } 111 | 112 | return ports, nil 113 | } 114 | 115 | // excludePorts excludes the list of ports from the exclusion list 116 | func excludePorts(options *Options, ports []*port.Port) ([]*port.Port, error) { 117 | if options.ExcludePorts == "" { 118 | return ports, nil 119 | } 120 | 121 | var filteredPorts []*port.Port 122 | 123 | // Exclude the ports specified by the user in exclusion list 124 | excludedPortsCLI, err := parsePortsList(options.ExcludePorts) 125 | if err != nil { 126 | return nil, fmt.Errorf("could not read exclusion ports: %s", err) 127 | } 128 | 129 | for _, port := range ports { 130 | found := false 131 | for _, excludedPort := range excludedPortsCLI { 132 | if excludedPort.Port == port.Port && excludedPort.Protocol == port.Protocol { 133 | found = true 134 | break 135 | } 136 | } 137 | if !found { 138 | filteredPorts = append(filteredPorts, port) 139 | } 140 | } 141 | return filteredPorts, nil 142 | } 143 | 144 | func parsePortsSlice(ranges []string) ([]*port.Port, error) { 145 | var ports []*port.Port 146 | for _, r := range ranges { 147 | r = strings.TrimSpace(r) 148 | 149 | portProtocol := protocol.TCP 150 | if strings.HasPrefix(r, "u:") { 151 | portProtocol = protocol.UDP 152 | r = strings.TrimPrefix(r, "u:") 153 | } 154 | 155 | if strings.Contains(r, "-") { 156 | parts := strings.Split(r, "-") 157 | if len(parts) != portListStrParts { 158 | return nil, fmt.Errorf("invalid port selection segment: '%s'", r) 159 | } 160 | 161 | p1, err := strconv.Atoi(parts[0]) 162 | if err != nil { 163 | return nil, fmt.Errorf("invalid port number: '%s'", parts[0]) 164 | } 165 | 166 | p2, err := strconv.Atoi(parts[1]) 167 | if err != nil { 168 | return nil, fmt.Errorf("invalid port number: '%s'", parts[1]) 169 | } 170 | 171 | if p1 > p2 || p2 > 65535 { 172 | return nil, fmt.Errorf("invalid port range: %d-%d", p1, p2) 173 | } 174 | 175 | for i := p1; i <= p2; i++ { 176 | port := &port.Port{Port: i, Protocol: portProtocol} 177 | ports = append(ports, port) 178 | } 179 | } else { 180 | portNumber, err := strconv.Atoi(r) 181 | if err != nil || portNumber > 65535 { 182 | return nil, fmt.Errorf("invalid port number: '%s'", r) 183 | } 184 | port := &port.Port{Port: portNumber, Protocol: portProtocol} 185 | ports = append(ports, port) 186 | } 187 | } 188 | 189 | // dedupe ports 190 | seen := make(map[string]struct{}) 191 | var dedupedPorts []*port.Port 192 | for _, port := range ports { 193 | if _, ok := seen[port.String()]; ok { 194 | continue 195 | } 196 | seen[port.String()] = struct{}{} 197 | dedupedPorts = append(dedupedPorts, port) 198 | } 199 | 200 | return dedupedPorts, nil 201 | } 202 | 203 | func parsePortsList(data string) ([]*port.Port, error) { 204 | return parsePortsSlice(strings.Split(data, ",")) 205 | } 206 | 207 | func merge(slices ...[]*port.Port) []*port.Port { 208 | var result []*port.Port 209 | for _, slice := range slices { 210 | result = append(result, slice...) 211 | } 212 | return result 213 | } 214 | -------------------------------------------------------------------------------- /pkg/runner/ports_test.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/projectdiscovery/naabu/v2/pkg/port" 8 | "github.com/projectdiscovery/naabu/v2/pkg/protocol" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestParsePortsList(t *testing.T) { 13 | tests := []struct { 14 | args string 15 | want []*port.Port 16 | wantErr bool 17 | }{ 18 | {"1,2,3,4", []*port.Port{{Port: 1, Protocol: protocol.TCP}, {Port: 2, Protocol: protocol.TCP}, {Port: 3, Protocol: protocol.TCP}, {Port: 4, Protocol: protocol.TCP}}, false}, 19 | {"1-3,10", []*port.Port{{Port: 1, Protocol: protocol.TCP}, {Port: 2, Protocol: protocol.TCP}, {Port: 3, Protocol: protocol.TCP}, {Port: 10, Protocol: protocol.TCP}}, false}, 20 | {"17,17,17,18", []*port.Port{{Port: 17, Protocol: protocol.TCP}, {Port: 18, Protocol: protocol.TCP}}, false}, 21 | {"a", nil, true}, 22 | } 23 | for _, tt := range tests { 24 | t.Run("", func(t *testing.T) { 25 | got, err := parsePortsList(tt.args) 26 | if (err != nil) != tt.wantErr { 27 | t.Errorf("parsePortsList() error = %v, wantErr %v", err, tt.wantErr) 28 | return 29 | } 30 | if !reflect.DeepEqual(got, tt.want) { 31 | t.Errorf("parsePortsList() = %v, want %v", got, tt.want) 32 | } 33 | }) 34 | } 35 | } 36 | 37 | func TestExcludePorts(t *testing.T) { 38 | var options Options 39 | ports := []*port.Port{ 40 | {Port: 1, Protocol: protocol.TCP}, 41 | {Port: 10, Protocol: protocol.TCP}, 42 | } 43 | 44 | // no filtering 45 | filteredPorts, err := excludePorts(&options, ports) 46 | assert.Nil(t, err) 47 | assert.EqualValues(t, filteredPorts, ports) 48 | 49 | // invalid filter 50 | options.ExcludePorts = "a" 51 | _, err = excludePorts(&options, ports) 52 | assert.NotNil(t, err) 53 | 54 | // valid filter 55 | options.ExcludePorts = "1" 56 | filteredPorts, err = excludePorts(&options, ports) 57 | assert.Nil(t, err) 58 | expectedPorts := []*port.Port{ 59 | {Port: 10, Protocol: protocol.TCP}, 60 | } 61 | assert.EqualValues(t, expectedPorts, filteredPorts) 62 | } 63 | 64 | func TestParsePorts(t *testing.T) { 65 | // top ports 66 | tests := []struct { 67 | args string 68 | want int 69 | wantErr bool 70 | }{ 71 | {"full", 65535, false}, 72 | {"100", 100, false}, 73 | {"1000", 1000, false}, 74 | {"a", 0, true}, 75 | } 76 | for _, tt := range tests { 77 | t.Run(tt.args, func(t *testing.T) { 78 | var options Options 79 | options.TopPorts = tt.args 80 | got, err := ParsePorts(&options) 81 | if tt.wantErr { 82 | assert.NotNil(t, err) 83 | } else { 84 | assert.Nil(t, err) 85 | } 86 | assert.Equal(t, tt.want, len(got)) 87 | }) 88 | } 89 | 90 | // ports 91 | tests = []struct { 92 | args string 93 | want int 94 | wantErr bool 95 | }{ 96 | {"-", 65535, false}, 97 | {"a", 0, true}, 98 | {"1,2,4-10", 9, false}, 99 | } 100 | for _, tt := range tests { 101 | t.Run(tt.args, func(t *testing.T) { 102 | var options Options 103 | options.Ports = tt.args 104 | got, err := ParsePorts(&options) 105 | if tt.wantErr { 106 | assert.NotNil(t, err) 107 | } else { 108 | assert.Nil(t, err) 109 | } 110 | assert.Equal(t, tt.want, len(got)) 111 | }) 112 | } 113 | 114 | // default to 100 ports 115 | got, err := ParsePorts(&Options{}) 116 | assert.Nil(t, err) 117 | assert.Equal(t, 100, len(got)) 118 | } 119 | -------------------------------------------------------------------------------- /pkg/runner/resume.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | "sync" 8 | 9 | "github.com/projectdiscovery/gologger" 10 | fileutil "github.com/projectdiscovery/utils/file" 11 | permissionutil "github.com/projectdiscovery/utils/permission" 12 | ) 13 | 14 | // Default resume file 15 | const defaultResumeFileName = "resume.cfg" 16 | 17 | // DefaultResumeFolderPath returns the default resume folder path 18 | func DefaultResumeFolderPath() string { 19 | home, err := os.UserHomeDir() 20 | if err != nil { 21 | return defaultResumeFileName 22 | } 23 | return filepath.Join(home, ".config", "naabu") 24 | } 25 | 26 | // DefaultResumeFilePath returns the default resume file full path 27 | func DefaultResumeFilePath() string { 28 | return filepath.Join(DefaultResumeFolderPath(), defaultResumeFileName) 29 | } 30 | 31 | // ResumeCfg contains the scan progression 32 | type ResumeCfg struct { 33 | sync.RWMutex 34 | Retry int `json:"retry"` 35 | Seed int64 `json:"seed"` 36 | Index int64 `json:"index"` 37 | } 38 | 39 | // NewResumeCfg creates a new scan progression structure 40 | func NewResumeCfg() *ResumeCfg { 41 | return &ResumeCfg{} 42 | } 43 | 44 | // SaveResumeConfig to file 45 | func (resumeCfg *ResumeCfg) SaveResumeConfig() error { 46 | resumeCfg.RLock() 47 | defer resumeCfg.RUnlock() 48 | 49 | data, err := json.MarshalIndent(resumeCfg, "", "\t") 50 | if err != nil { 51 | return err 52 | } 53 | resumeFolderPath := DefaultResumeFolderPath() 54 | if !fileutil.FolderExists(resumeFolderPath) { 55 | _ = fileutil.CreateFolder(DefaultResumeFolderPath()) 56 | } 57 | 58 | return os.WriteFile(DefaultResumeFilePath(), data, permissionutil.ConfigFilePermission) 59 | } 60 | 61 | // ConfigureResume read the resume config file 62 | func (resumeCfg *ResumeCfg) ConfigureResume() error { 63 | resumeCfg.RLock() 64 | defer resumeCfg.RUnlock() 65 | 66 | gologger.Info().Msg("Resuming from save checkpoint") 67 | file, err := os.ReadFile(DefaultResumeFilePath()) 68 | if err != nil { 69 | return err 70 | } 71 | err = json.Unmarshal([]byte(file), &resumeCfg) 72 | if err != nil { 73 | return err 74 | } 75 | return nil 76 | } 77 | 78 | // ShouldSaveResume file 79 | func (resumeCfg *ResumeCfg) ShouldSaveResume() bool { 80 | return true 81 | } 82 | 83 | // CleanupResumeConfig cleaning up the config file 84 | func (resumeCfg *ResumeCfg) CleanupResumeConfig() { 85 | if fileutil.FileExists(DefaultResumeFilePath()) { 86 | _ = os.Remove(DefaultResumeFilePath()) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pkg/runner/targets.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "net" 9 | "os" 10 | "strings" 11 | 12 | "github.com/projectdiscovery/gologger" 13 | "github.com/projectdiscovery/mapcidr/asn" 14 | "github.com/projectdiscovery/naabu/v2/pkg/privileges" 15 | "github.com/projectdiscovery/naabu/v2/pkg/scan" 16 | iputil "github.com/projectdiscovery/utils/ip" 17 | readerutil "github.com/projectdiscovery/utils/reader" 18 | "github.com/remeh/sizedwaitgroup" 19 | ) 20 | 21 | func (r *Runner) Load() error { 22 | r.scanner.ListenHandler.Phase.Set(scan.Init) 23 | 24 | // merge all target sources into a file 25 | targetfile, err := r.mergeToFile() 26 | if err != nil { 27 | return err 28 | } 29 | r.targetsFile = targetfile 30 | 31 | // pre-process all targets (resolves all non fqdn targets to ip address) 32 | err = r.PreProcessTargets() 33 | if err != nil { 34 | gologger.Warning().Msgf("%s\n", err) 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (r *Runner) mergeToFile() (string, error) { 41 | // merge all targets in a unique file 42 | tempInput, err := os.CreateTemp("", "stdin-input-*") 43 | if err != nil { 44 | return "", err 45 | } 46 | defer func() { 47 | if err := tempInput.Close(); err != nil { 48 | gologger.Error().Msgf("Could not close temp input: %s\n", err) 49 | } 50 | }() 51 | 52 | // target defined via CLI argument 53 | if len(r.options.Host) > 0 { 54 | for _, v := range r.options.Host { 55 | _, _ = fmt.Fprintf(tempInput, "%s\n", v) 56 | } 57 | } 58 | 59 | // Targets from file 60 | if r.options.HostsFile != "" { 61 | f, err := os.Open(r.options.HostsFile) 62 | if err != nil { 63 | return "", err 64 | } 65 | defer func() { 66 | if err := f.Close(); err != nil { 67 | gologger.Error().Msgf("Could not close file %s: %s\n", r.options.HostsFile, err) 68 | } 69 | }() 70 | if _, err := io.Copy(tempInput, f); err != nil { 71 | return "", err 72 | } 73 | } 74 | 75 | // targets from STDIN 76 | if r.options.Stdin { 77 | timeoutReader := readerutil.TimeoutReader{Reader: os.Stdin, Timeout: r.options.InputReadTimeout} 78 | if _, err := io.Copy(tempInput, timeoutReader); err != nil { 79 | return "", err 80 | } 81 | } 82 | 83 | // all additional non-named cli arguments are interpreted as targets 84 | for _, target := range flag.Args() { 85 | _, _ = fmt.Fprintf(tempInput, "%s\n", target) 86 | } 87 | 88 | filename := tempInput.Name() 89 | return filename, nil 90 | } 91 | 92 | func (r *Runner) PreProcessTargets() error { 93 | if r.options.Stream { 94 | defer close(r.streamChannel) 95 | } 96 | wg := sizedwaitgroup.New(r.options.Threads) 97 | f, err := os.Open(r.targetsFile) 98 | if err != nil { 99 | return err 100 | } 101 | defer func() { 102 | if err := f.Close(); err != nil { 103 | gologger.Error().Msgf("Could not close file %s: %s\n", r.targetsFile, err) 104 | } 105 | }() 106 | s := bufio.NewScanner(f) 107 | for s.Scan() { 108 | wg.Add() 109 | func(target string) { 110 | defer wg.Done() 111 | if err := r.AddTarget(target); err != nil { 112 | gologger.Warning().Msgf("%s\n", err) 113 | } 114 | }(s.Text()) 115 | } 116 | 117 | wg.Wait() 118 | return nil 119 | } 120 | 121 | func (r *Runner) AddTarget(target string) error { 122 | target = strings.TrimSpace(target) 123 | if target == "" { 124 | return nil 125 | } 126 | if asn.IsASN(target) { 127 | // Get CIDRs for ASN 128 | cidrs, err := asn.GetCIDRsForASNNum(target) 129 | if err != nil { 130 | return err 131 | } 132 | for _, cidr := range cidrs { 133 | if r.options.Stream { 134 | r.streamChannel <- Target{Cidr: cidr.String()} 135 | } else if err := r.scanner.IPRanger.AddHostWithMetadata(cidr.String(), "cidr"); err != nil { // Add cidr directly to ranger, as single ips would allocate more resources later 136 | gologger.Warning().Msgf("%s\n", err) 137 | } 138 | } 139 | return nil 140 | } 141 | if iputil.IsCIDR(target) { 142 | if r.options.Stream { 143 | r.streamChannel <- Target{Cidr: target} 144 | } else if err := r.scanner.IPRanger.AddHostWithMetadata(target, "cidr"); err != nil { // Add cidr directly to ranger, as single ips would allocate more resources later 145 | gologger.Warning().Msgf("%s\n", err) 146 | } 147 | return nil 148 | } 149 | if iputil.IsIP(target) && !r.scanner.IPRanger.Contains(target) { 150 | ip := net.ParseIP(target) 151 | // convert ip4 expressed as ip6 back to ip4 152 | if ip.To4() != nil { 153 | target = ip.To4().String() 154 | } 155 | if r.options.Stream { 156 | r.streamChannel <- Target{Cidr: iputil.ToCidr(target).String()} 157 | } else { 158 | metadata := "ip" 159 | if r.options.ReversePTR { 160 | names, err := iputil.ToFQDN(target) 161 | if err != nil { 162 | gologger.Debug().Msgf("reverse ptr failed for %s: %s\n", target, err) 163 | } else { 164 | metadata = strings.Trim(names[0], ".") 165 | } 166 | } 167 | err := r.scanner.IPRanger.AddHostWithMetadata(target, metadata) 168 | if err != nil { 169 | gologger.Warning().Msgf("%s\n", err) 170 | } 171 | } 172 | return nil 173 | } 174 | 175 | host, port, hasPort := getPort(target) 176 | 177 | targetToResolve := target 178 | if hasPort { 179 | targetToResolve = host 180 | } 181 | ips, err := r.resolveFQDN(targetToResolve) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | for _, ip := range ips { 187 | if r.options.Stream { 188 | if hasPort { 189 | r.streamChannel <- Target{Ip: ip, Port: port} 190 | if len(r.options.Ports) > 0 { 191 | r.streamChannel <- Target{Cidr: iputil.ToCidr(ip).String()} 192 | if err := r.scanner.IPRanger.AddHostWithMetadata(joinHostPort(ip, ""), target); err != nil { 193 | gologger.Warning().Msgf("%s\n", err) 194 | } 195 | } 196 | } else { 197 | r.streamChannel <- Target{Cidr: iputil.ToCidr(ip).String()} 198 | if err := r.scanner.IPRanger.AddHostWithMetadata(joinHostPort(ip, port), target); err != nil { 199 | gologger.Warning().Msgf("%s\n", err) 200 | } 201 | } 202 | } else if hasPort { 203 | if len(r.options.Ports) > 0 { 204 | if err := r.scanner.IPRanger.AddHostWithMetadata(joinHostPort(ip, ""), target); err != nil { 205 | gologger.Warning().Msgf("%s\n", err) 206 | } 207 | } else { 208 | if err := r.scanner.IPRanger.AddHostWithMetadata(joinHostPort(ip, port), target); err != nil { 209 | gologger.Warning().Msgf("%s\n", err) 210 | } 211 | } 212 | } else if err := r.scanner.IPRanger.AddHostWithMetadata(ip, target); err != nil { 213 | gologger.Warning().Msgf("%s\n", err) 214 | } 215 | } 216 | 217 | return nil 218 | } 219 | 220 | func joinHostPort(host, port string) string { 221 | if port == "" { 222 | return host 223 | } 224 | 225 | return net.JoinHostPort(host, port) 226 | } 227 | 228 | func (r *Runner) resolveFQDN(target string) ([]string, error) { 229 | ipsV4, ipsV6, err := r.host2ips(target) 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | var ( 235 | initialHosts []string 236 | initialHostsV6 []string 237 | hostIPS []string 238 | ) 239 | for _, ip := range ipsV4 { 240 | if !r.scanner.IPRanger.Np.ValidateAddress(ip) { 241 | gologger.Warning().Msgf("Skipping host %s as ip %s was excluded\n", target, ip) 242 | continue 243 | } 244 | 245 | initialHosts = append(initialHosts, ip) 246 | } 247 | for _, ip := range ipsV6 { 248 | if !r.scanner.IPRanger.Np.ValidateAddress(ip) { 249 | gologger.Warning().Msgf("Skipping host %s as ip %s was excluded\n", target, ip) 250 | continue 251 | } 252 | 253 | initialHostsV6 = append(initialHostsV6, ip) 254 | } 255 | if len(initialHosts) == 0 && len(initialHostsV6) == 0 { 256 | return []string{}, nil 257 | } 258 | 259 | // If the user has specified ping probes, perform ping on addresses 260 | if privileges.IsPrivileged && r.options.Ping && len(initialHosts) > 1 { 261 | // Scan the hosts found for ping probes 262 | pingResults, err := scan.PingHosts(initialHosts) 263 | if err != nil { 264 | gologger.Warning().Msgf("Could not perform ping scan on %s: %s\n", target, err) 265 | return []string{}, err 266 | } 267 | for _, result := range pingResults.Hosts { 268 | if result.Type == scan.HostActive { 269 | gologger.Debug().Msgf("Ping probe succeed for %s: latency=%s\n", result.Host, result.Latency) 270 | } else { 271 | gologger.Debug().Msgf("Ping probe failed for %s: error=%s\n", result.Host, result.Error) 272 | } 273 | } 274 | 275 | // Get the fastest host in the list of hosts 276 | fastestHost, err := pingResults.GetFastestHost() 277 | if err != nil { 278 | gologger.Warning().Msgf("No active host found for %s: %s\n", target, err) 279 | return []string{}, err 280 | } 281 | gologger.Info().Msgf("Fastest host found for target: %s (%s)\n", fastestHost.Host, fastestHost.Latency) 282 | hostIPS = append(hostIPS, fastestHost.Host) 283 | } else if r.options.ScanAllIPS { 284 | hostIPS = append(initialHosts, initialHostsV6...) 285 | } else { 286 | if len(initialHosts) > 0 { 287 | hostIPS = append(hostIPS, initialHosts[0]) 288 | } 289 | if len(initialHostsV6) > 0 { 290 | hostIPS = append(hostIPS, initialHostsV6[0]) 291 | } 292 | } 293 | 294 | for _, hostIP := range hostIPS { 295 | if r.scanner.IPRanger.Contains(hostIP) { 296 | gologger.Debug().Msgf("Using ip %s for host %s enumeration\n", hostIP, target) 297 | } 298 | } 299 | 300 | return hostIPS, nil 301 | } 302 | -------------------------------------------------------------------------------- /pkg/runner/targets_test.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/projectdiscovery/ipranger" 8 | "github.com/projectdiscovery/naabu/v2/pkg/scan" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_AddTarget(t *testing.T) { 13 | ipranger, _ := ipranger.New() 14 | defer func() { 15 | if err := ipranger.Close(); err != nil { 16 | log.Printf("could not close ipranger: %s\n", err) 17 | } 18 | }() 19 | 20 | r := &Runner{ 21 | options: &Options{}, 22 | scanner: &scan.Scanner{IPRanger: ipranger}, 23 | } 24 | 25 | // IPV6 Compressed should generate a warning 26 | err := r.AddTarget("::ffff:c0a8:101") 27 | require.Nil(t, err, "compressed ipv6 incorrectly parsed") 28 | 29 | // IPV6 Expanded (Shortened) 30 | err = r.AddTarget("0:0:0:0:0:ffff:c0a8:0101") 31 | require.Nil(t, err, "expanded shortened ipv6 incorrectly parsed") 32 | 33 | // IPV6 Expanded 34 | err = r.AddTarget("0000:0000:0000:0000:0000:ffff:c0a8:0101") 35 | require.Nil(t, err, "fully expanded ipv6 incorrectly parsed") 36 | 37 | // IPV4 38 | err = r.AddTarget("127.0.0.1") 39 | require.Nil(t, err, "ipv4 incorrectly parsed") 40 | 41 | // IPV4 cidr 42 | err = r.AddTarget("127.0.0.1/24") 43 | require.Nil(t, err, "ipv4 cidr incorrectly parsed") 44 | 45 | // todo: excluding due to api instability (https://github.com/projectdiscovery/asnmap/issues/198) 46 | // err = r.AddTarget("AS14421") 47 | // require.Nil(t, err, "ASN incorrectly parsed") 48 | } 49 | -------------------------------------------------------------------------------- /pkg/runner/util.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/projectdiscovery/gologger" 8 | "github.com/projectdiscovery/naabu/v2/pkg/scan" 9 | iputil "github.com/projectdiscovery/utils/ip" 10 | osutil "github.com/projectdiscovery/utils/os" 11 | sliceutil "github.com/projectdiscovery/utils/slice" 12 | ) 13 | 14 | func (r *Runner) host2ips(target string) (targetIPsV4 []string, targetIPsV6 []string, err error) { 15 | // If the host is a Domain, then perform resolution and discover all IP 16 | // addresses for a given host. Else use that host for port scanning 17 | if !iputil.IsIP(target) { 18 | dnsData, err := r.dnsclient.QueryMultiple(target) 19 | if err != nil || dnsData == nil { 20 | gologger.Warning().Msgf("Could not get IP for host: %s\n", target) 21 | return nil, nil, err 22 | } 23 | if len(r.options.IPVersion) > 0 { 24 | if sliceutil.Contains(r.options.IPVersion, scan.IPv4) { 25 | targetIPsV4 = append(targetIPsV4, dnsData.A...) 26 | } 27 | if sliceutil.Contains(r.options.IPVersion, scan.IPv6) { 28 | targetIPsV6 = append(targetIPsV6, dnsData.AAAA...) 29 | } 30 | } else { 31 | targetIPsV4 = append(targetIPsV4, dnsData.A...) 32 | } 33 | if len(targetIPsV4) == 0 && len(targetIPsV6) == 0 { 34 | return targetIPsV4, targetIPsV6, fmt.Errorf("no IP addresses found for host: %s", target) 35 | } 36 | } else { 37 | targetIPsV4 = append(targetIPsV6, target) 38 | gologger.Debug().Msgf("Found %d addresses for %s\n", len(targetIPsV4), target) 39 | } 40 | 41 | return 42 | } 43 | 44 | func isOSSupported() bool { 45 | return osutil.IsLinux() || osutil.IsOSX() 46 | } 47 | 48 | func getPort(target string) (string, string, bool) { 49 | host, port, err := net.SplitHostPort(target) 50 | if err == nil && iputil.IsPort(port) { 51 | return host, port, true 52 | } 53 | 54 | return target, "", false 55 | } 56 | -------------------------------------------------------------------------------- /pkg/runner/util_test.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/projectdiscovery/dnsx/libs/dnsx" 7 | "github.com/projectdiscovery/naabu/v2/pkg/scan" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_host2ips(t *testing.T) { 13 | tests := []struct { 14 | args string 15 | want []string 16 | wantV6 []string 17 | wantErr bool 18 | }{ 19 | {"10.10.10.10", []string{"10.10.10.10"}, nil, false}, 20 | {"localhost", []string{"127.0.0.1"}, []string{"::1"}, false}, // some linux distribution don't have ::1 in /etc/hosts 21 | {"aaaa", nil, nil, true}, 22 | {"10.10.10.0/24", nil, nil, true}, 23 | } 24 | 25 | r, err := NewRunner(&Options{IPVersion: []string{scan.IPv4, scan.IPv6}, Retries: 1}) 26 | require.Nil(t, err) 27 | dnsclient, err := dnsx.New(dnsx.DefaultOptions) 28 | require.Nil(t, err) 29 | r.dnsclient = dnsclient 30 | 31 | for _, tt := range tests { 32 | t.Run(tt.args, func(t *testing.T) { 33 | var options Options 34 | options.TopPorts = tt.args 35 | got, gotV6, err := r.host2ips(tt.args) 36 | if tt.wantErr { 37 | assert.NotNil(t, err) 38 | } else { 39 | assert.Nil(t, err) 40 | } 41 | assert.Equal(t, tt.want, got) 42 | // As some distributions don't handle correctly ipv6 we compare results only if necessary 43 | if len(gotV6) > 0 && len(tt.wantV6) > 0 { 44 | assert.Equal(t, tt.wantV6, gotV6) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/runner/validate.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/projectdiscovery/naabu/v2/pkg/port" 11 | "github.com/projectdiscovery/naabu/v2/pkg/privileges" 12 | "github.com/projectdiscovery/naabu/v2/pkg/scan" 13 | fileutil "github.com/projectdiscovery/utils/file" 14 | iputil "github.com/projectdiscovery/utils/ip" 15 | osutil "github.com/projectdiscovery/utils/os" 16 | sliceutil "github.com/projectdiscovery/utils/slice" 17 | 18 | "github.com/projectdiscovery/gologger" 19 | "github.com/projectdiscovery/gologger/formatter" 20 | "github.com/projectdiscovery/gologger/levels" 21 | ) 22 | 23 | var ( 24 | errNoInputList = errors.New("no input list provided") 25 | errOutputMode = errors.New("both verbose and silent mode specified") 26 | errZeroValue = errors.New("cannot be zero") 27 | errTwoOutputMode = errors.New("both json and csv mode specified") 28 | ) 29 | 30 | // ValidateOptions validates the configuration options passed 31 | func (options *Options) ValidateOptions() error { 32 | // Check if Host, list of domains, or stdin info was provided. 33 | // If none was provided, then return. 34 | if options.Host == nil && options.HostsFile == "" && !options.Stdin && len(flag.Args()) == 0 { 35 | return errNoInputList 36 | } 37 | 38 | if (options.WithHostDiscovery || options.OnlyHostDiscovery) && options.ScanType != SynScan { 39 | gologger.Warning().Msgf("host discovery requires syn scan, automatically switching to syn scan") 40 | options.ScanType = SynScan 41 | } 42 | 43 | // Both verbose and silent flags were used 44 | if options.Verbose && options.Silent { 45 | return errOutputMode 46 | } 47 | 48 | if options.JSON && options.CSV { 49 | return errTwoOutputMode 50 | } 51 | 52 | if options.Rate == 0 { 53 | return errors.Wrap(errZeroValue, "rate") 54 | } else if !privileges.IsPrivileged && options.Rate == DefaultRateSynScan { 55 | options.Rate = DefaultRateConnectScan 56 | } 57 | 58 | if !privileges.IsPrivileged && options.Retries == DefaultRetriesSynScan { 59 | options.Retries = DefaultRetriesConnectScan 60 | } 61 | 62 | if options.Interface != "" { 63 | if _, err := net.InterfaceByName(options.Interface); err != nil { 64 | return fmt.Errorf("interface %s not found", options.Interface) 65 | } 66 | } 67 | 68 | if fileutil.FileExists(options.Resolvers) { 69 | chanResolvers, err := fileutil.ReadFile(options.Resolvers) 70 | if err != nil { 71 | return err 72 | } 73 | for resolver := range chanResolvers { 74 | options.baseResolvers = append(options.baseResolvers, resolver) 75 | } 76 | } else if options.Resolvers != "" { 77 | for _, resolver := range strings.Split(options.Resolvers, ",") { 78 | options.baseResolvers = append(options.baseResolvers, strings.TrimSpace(resolver)) 79 | } 80 | } 81 | 82 | // passive mode enables automatically stream 83 | if options.Passive { 84 | options.Stream = true 85 | } 86 | 87 | // stream 88 | if options.Stream { 89 | if options.Resume { 90 | return errors.New("resume not supported in stream active mode") 91 | } 92 | if options.EnableProgressBar { 93 | return errors.New("stats not supported in stream active mode") 94 | } 95 | if options.Nmap { 96 | return errors.New("nmap not supported in stream active mode") 97 | } 98 | } 99 | 100 | // stream passive 101 | if options.Verify && options.Stream && !options.Passive { 102 | return errors.New("verify not supported in stream active mode") 103 | } 104 | 105 | // Parse and validate source ip and source port 106 | // checks if source ip is ip only 107 | isOnlyIP := iputil.IsIP(options.SourceIP) 108 | if options.SourceIP != "" && !isOnlyIP { 109 | ip, port, err := net.SplitHostPort(options.SourceIP) 110 | if err != nil { 111 | return err 112 | } 113 | options.SourceIP = ip 114 | options.SourcePort = port 115 | } 116 | 117 | if len(options.IPVersion) > 0 && !sliceutil.ContainsItems([]string{scan.IPv4, scan.IPv6}, options.IPVersion) { 118 | return errors.New("IP Version must be 4 and/or 6") 119 | } 120 | // Return error if any host discovery releated option is provided but host discovery is disabled 121 | if !options.WithHostDiscovery && options.hasProbes() { 122 | return errors.New("discovery probes were provided but host discovery is disabled") 123 | } 124 | 125 | // Host Discovery mode needs provileged access 126 | if options.OnlyHostDiscovery && !privileges.IsPrivileged { 127 | if osutil.IsWindows() { 128 | return errors.New("host discovery not (yet) supported on windows") 129 | } 130 | return errors.New("sudo access required to perform host discovery") 131 | } 132 | 133 | if options.PortThreshold < 0 || options.PortThreshold > 65535 { 134 | return errors.New("port threshold must be between 0 and 65535") 135 | } 136 | 137 | if options.Proxy != "" && options.ScanType == SynScan { 138 | gologger.Warning().Msgf("Syn Scan can't be used with socks proxy: falling back to connect scan") 139 | options.ScanType = ConnectScan 140 | } 141 | 142 | if options.ScanType == SynScan && scan.PkgRouter == nil { 143 | gologger.Warning().Msgf("Routing could not be determined (are you using a VPN?).falling back to connect scan") 144 | options.ScanType = ConnectScan 145 | } 146 | 147 | if options.ServiceDiscovery || options.ServiceVersion { 148 | return errors.New("service discovery feature is not implemented") 149 | } 150 | 151 | return nil 152 | } 153 | 154 | // configureOutput configures the output on the screen 155 | func (options *Options) configureOutput() { 156 | if options.Verbose { 157 | gologger.DefaultLogger.SetMaxLevel(levels.LevelVerbose) 158 | } 159 | if options.Debug { 160 | gologger.DefaultLogger.SetMaxLevel(levels.LevelDebug) 161 | } 162 | if options.NoColor { 163 | gologger.DefaultLogger.SetFormatter(formatter.NewCLI(true)) 164 | } 165 | if options.Silent { 166 | gologger.DefaultLogger.SetMaxLevel(levels.LevelSilent) 167 | } 168 | } 169 | 170 | // ConfigureHostDiscovery enables default probes if none is specified 171 | // but host discovery option was requested 172 | func (options *Options) configureHostDiscovery(ports []*port.Port) { 173 | // if less than two ports are specified as input, reduce time and scan directly 174 | if len(ports) <= 2 { 175 | gologger.Info().Msgf("Host discovery disabled: less than two ports were specified") 176 | options.WithHostDiscovery = false 177 | } 178 | if options.shouldDiscoverHosts() && !options.hasProbes() { 179 | // if no options were defined enable 180 | // - ICMP Echo Request 181 | // - ICMP timestamp 182 | // - TCP SYN on port 80 183 | // - TCP SYN on port 443 184 | // - TCP ACK on port 80 185 | // - TCP ACK on port 443 186 | options.IcmpEchoRequestProbe = true 187 | options.IcmpTimestampRequestProbe = true 188 | options.TcpSynPingProbes = append(options.TcpSynPingProbes, "80") 189 | options.TcpSynPingProbes = append(options.TcpSynPingProbes, "443") 190 | options.TcpAckPingProbes = append(options.TcpAckPingProbes, "80") 191 | options.TcpAckPingProbes = append(options.TcpAckPingProbes, "443") 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /pkg/runner/validate_test.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestOptions(t *testing.T) { 11 | options := Options{} 12 | assert.ErrorIs(t, errNoInputList, options.ValidateOptions()) 13 | 14 | options.Host = []string{"target1", "target2"} 15 | options.Timeout = 2 16 | assert.EqualError(t, options.ValidateOptions(), errors.Wrap(errZeroValue, "rate").Error()) 17 | 18 | options.Resolvers = "aaabbbccc" 19 | assert.NotNil(t, options.ValidateOptions()) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/scan/arp_unix.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin 2 | 3 | package scan 4 | 5 | import ( 6 | "errors" 7 | "net" 8 | 9 | "github.com/gopacket/gopacket" 10 | "github.com/gopacket/gopacket/layers" 11 | "github.com/projectdiscovery/gologger" 12 | ) 13 | 14 | func init() { 15 | ArpRequestAsync = arpRequestAsync 16 | } 17 | 18 | // ArpRequestAsync asynchronous to the target ip address 19 | func arpRequestAsync(ip string) { 20 | networkInterface, _, sourceIP, err := PkgRouter.Route(net.ParseIP(ip)) 21 | if networkInterface == nil { 22 | err = errors.New("Could not send ARP Request packet to " + ip + ": no interface with outbound source found") 23 | } 24 | if err != nil { 25 | gologger.Debug().Msgf("%s\n", err) 26 | return 27 | } 28 | // network layers 29 | eth := layers.Ethernet{ 30 | SrcMAC: networkInterface.HardwareAddr, 31 | DstMAC: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, 32 | EthernetType: layers.EthernetTypeARP, 33 | } 34 | arp := layers.ARP{ 35 | AddrType: layers.LinkTypeEthernet, 36 | Protocol: layers.EthernetTypeIPv4, 37 | HwAddressSize: 6, 38 | ProtAddressSize: 4, 39 | Operation: layers.ARPRequest, 40 | SourceHwAddress: []byte(networkInterface.HardwareAddr), 41 | SourceProtAddress: sourceIP.To4(), 42 | DstHwAddress: []byte{0, 0, 0, 0, 0, 0}, 43 | DstProtAddress: net.ParseIP(ip).To4(), 44 | } 45 | 46 | // Set up buffer and options for serialization. 47 | buf := gopacket.NewSerializeBuffer() 48 | opts := gopacket.SerializeOptions{ 49 | FixLengths: true, 50 | ComputeChecksums: true, 51 | } 52 | 53 | err = gopacket.SerializeLayers(buf, opts, ð, &arp) 54 | if err != nil { 55 | gologger.Warning().Msgf("%s\n", err) 56 | return 57 | } 58 | // send the packet out on every interface 59 | for _, handler := range handlers.EthernetActive { 60 | err := handler.WritePacketData(buf.Bytes()) 61 | if err != nil { 62 | gologger.Warning().Msgf("%s\n", err) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/scan/arp_win.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package scan 4 | 5 | func init() { 6 | ArpRequestAsync = func(ip string) {} 7 | } 8 | -------------------------------------------------------------------------------- /pkg/scan/cdn.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/pkg/errors" 7 | iputil "github.com/projectdiscovery/utils/ip" 8 | ) 9 | 10 | // CdnCheck verifies if the given ip is part of Cdn/WAF ranges 11 | func (s *Scanner) CdnCheck(ip string) (bool, string, error) { 12 | if s.cdn == nil { 13 | return false, "", errors.New("cdn client not initialized") 14 | } 15 | if !iputil.IsIP(ip) { 16 | return false, "", errors.Errorf("%s is not a valid ip", ip) 17 | } 18 | 19 | // the goal is to check if ip is part of cdn/waf to decide if target should be scanned or not 20 | // since 'cloud' itemtype does not fit logic here , we consider target is not part of cdn/waf 21 | matched, value, itemType, err := s.cdn.Check(net.ParseIP((ip))) 22 | if itemType == "cloud" { 23 | return false, "", err 24 | } 25 | return matched, value, err 26 | } 27 | -------------------------------------------------------------------------------- /pkg/scan/cdn_test.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCdnCheck(t *testing.T) { 10 | s, err := NewScanner(&Options{ExcludeCdn: true}) 11 | assert.Nil(t, err) 12 | tests := []struct { 13 | args string 14 | want bool 15 | wantErr bool 16 | }{ 17 | {"192.168.1.1", false, false}, 18 | {"10.10.10.10", false, false}, 19 | {"aaaaa", false, true}, 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.args, func(t *testing.T) { 23 | isCdn, _, err := s.CdnCheck(tt.args) 24 | if tt.wantErr { 25 | assert.NotNil(t, err) 26 | } else { 27 | assert.Nil(t, err) 28 | } 29 | assert.Equal(t, tt.want, isCdn) 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/scan/connect.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/projectdiscovery/gologger" 8 | "github.com/projectdiscovery/naabu/v2/pkg/port" 9 | ) 10 | 11 | // ConnectVerify is used to verify if ports are accurate using a connect request 12 | func (s *Scanner) ConnectVerify(host string, ports []*port.Port) []*port.Port { 13 | var verifiedPorts []*port.Port 14 | for _, p := range ports { 15 | target := net.JoinHostPort(host, fmt.Sprint(p.Port)) 16 | conn, err := net.DialTimeout(p.Protocol.String(), target, s.timeout) 17 | if err != nil { 18 | continue 19 | } 20 | gologger.Debug().Msgf("Validated active port %d on %s\n", p.Port, host) 21 | _ = conn.Close() 22 | verifiedPorts = append(verifiedPorts, p) 23 | } 24 | return verifiedPorts 25 | } 26 | -------------------------------------------------------------------------------- /pkg/scan/connect_test.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "testing" 7 | 8 | "github.com/projectdiscovery/naabu/v2/pkg/port" 9 | "github.com/projectdiscovery/naabu/v2/pkg/protocol" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestConnectVerify(t *testing.T) { 14 | go func() { 15 | // start tcp server 16 | l, err := net.Listen("tcp", ":17895") 17 | if err != nil { 18 | assert.Nil(t, err) 19 | } 20 | defer func() { 21 | if err := l.Close(); err != nil { 22 | log.Printf("could not close listener: %s\n", err) 23 | } 24 | }() 25 | for { 26 | conn, err := l.Accept() 27 | if err != nil { 28 | return 29 | } 30 | defer func() { 31 | _ = conn.Close() 32 | }() 33 | } 34 | }() 35 | 36 | s, err := NewScanner(&Options{}) 37 | assert.Nil(t, err) 38 | wanted := []*port.Port{ 39 | {Port: 17895, Protocol: protocol.TCP}, 40 | } 41 | 42 | targetPorts := []*port.Port{ 43 | {Port: 17895, Protocol: protocol.TCP}, 44 | {Port: 17896, Protocol: protocol.TCP}, 45 | } 46 | got := s.ConnectVerify("localhost", targetPorts) 47 | assert.EqualValues(t, wanted, got) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/scan/externalip.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | // WhatsMyIP attempts to obtain the external ip through public api 10 | func WhatsMyIP() (string, error) { 11 | req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://api.ipify.org?format=text", nil) 12 | if err != nil { 13 | return "", nil 14 | } 15 | 16 | resp, err := http.DefaultClient.Do(req) 17 | if err != nil { 18 | return "", err 19 | } 20 | 21 | defer func() { 22 | _ = resp.Body.Close() 23 | }() 24 | ip, err := io.ReadAll(resp.Body) 25 | if err != nil { 26 | return "", err 27 | } 28 | 29 | return string(ip), nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/scan/externalip_test.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | // todo: temporary disabling the test as the third-party service is not working 4 | // func TestWhatsMyIP(t *testing.T) { 5 | // externalIp, err := WhatsMyIP() 6 | // assert.Nil(t, err) 7 | // assert.NotEmpty(t, externalIp) 8 | // } 9 | -------------------------------------------------------------------------------- /pkg/scan/icmp.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin 2 | 3 | package scan 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | "net" 9 | "os" 10 | "time" 11 | 12 | "github.com/projectdiscovery/gologger" 13 | iputil "github.com/projectdiscovery/utils/ip" 14 | "golang.org/x/net/icmp" 15 | "golang.org/x/net/ipv4" 16 | "golang.org/x/net/ipv6" 17 | ) 18 | 19 | const ( 20 | originTimestamp = 4 21 | receiveTimestamp = 8 22 | transmitTimestamp = 12 23 | ) 24 | 25 | func init() { 26 | pingIcmpEchoRequestCallback = PingIcmpEchoRequest 27 | pingIcmpTimestampRequestCallback = PingIcmpTimestampRequest 28 | } 29 | 30 | // PingIcmpEchoRequest synchronous to the target ip address 31 | func PingIcmpEchoRequest(ip string, timeout time.Duration) bool { 32 | destAddr := &net.IPAddr{IP: net.ParseIP(ip)} 33 | c, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") 34 | if err != nil { 35 | return false 36 | } 37 | defer func() { 38 | _ = c.Close() 39 | }() 40 | 41 | m := icmp.Message{ 42 | Type: ipv4.ICMPTypeEcho, 43 | Code: 0, 44 | Body: &icmp.Echo{ 45 | ID: os.Getpid() & 0xffff, 46 | Data: []byte(""), 47 | }, 48 | } 49 | 50 | data, err := m.Marshal(nil) 51 | if err != nil { 52 | return false 53 | } 54 | 55 | _, err = c.WriteTo(data, destAddr) 56 | if err != nil { 57 | return false 58 | } 59 | 60 | reply := make([]byte, 1500) 61 | err = c.SetReadDeadline(time.Now().Add(timeout)) 62 | if err != nil { 63 | return false 64 | } 65 | n, SourceIP, err := c.ReadFrom(reply) 66 | // timeout 67 | if err != nil { 68 | return false 69 | } 70 | // if anything is read from the connection it means that the host is alive 71 | if destAddr.String() == SourceIP.String() && n > 0 { 72 | return true 73 | } 74 | 75 | return false 76 | } 77 | 78 | // PingIcmpEchoRequestAsync asynchronous to the target ip address 79 | func PingIcmpEchoRequestAsync(ip string) { 80 | destinationIP := net.ParseIP(ip) 81 | var destAddr net.Addr 82 | m := icmp.Message{ 83 | Code: 0, 84 | Body: &icmp.Echo{ 85 | ID: os.Getpid() & 0xffff, 86 | Seq: 1, 87 | Data: []byte(""), 88 | }, 89 | } 90 | 91 | var packetListener net.PacketConn 92 | switch { 93 | case iputil.IsIPv4(ip): 94 | m.Type = ipv4.ICMPTypeEcho 95 | packetListener = icmpConn4 96 | destAddr = &net.IPAddr{IP: destinationIP} 97 | case iputil.IsIPv6(ip): 98 | m.Type = ipv6.ICMPTypeEchoRequest 99 | packetListener = icmpConn6 100 | networkInterface, _, _, err := PkgRouter.Route(destinationIP) 101 | if networkInterface == nil { 102 | err = fmt.Errorf("could not send ICMP Echo Request packet to %s: no interface with outbout source ipv6 found", destinationIP) 103 | } 104 | if err != nil { 105 | gologger.Debug().Msgf("%s\n", err) 106 | return 107 | } 108 | destAddr = &net.UDPAddr{IP: destinationIP, Zone: networkInterface.Name} 109 | } 110 | 111 | data, err := m.Marshal(nil) 112 | if err != nil { 113 | return 114 | } 115 | retries := 0 116 | send: 117 | if retries >= maxRetries { 118 | return 119 | } 120 | _, err = packetListener.WriteTo(data, destAddr) 121 | if err != nil { 122 | retries++ 123 | // introduce a small delay to allow the network interface to flush the queue 124 | time.Sleep(time.Duration(DeadlineSec) * time.Millisecond) 125 | goto send 126 | } 127 | } 128 | 129 | // PingIcmpTimestampRequest synchronous to the target ip address 130 | func PingIcmpTimestampRequest(ip string, timeout time.Duration) bool { 131 | destAddr := &net.IPAddr{IP: net.ParseIP(ip)} 132 | c, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") 133 | if err != nil { 134 | return false 135 | } 136 | defer func() { 137 | _ = c.Close() 138 | }() 139 | 140 | m := icmp.Message{ 141 | Type: ipv4.ICMPTypeTimestamp, 142 | Code: 0, 143 | Body: &Timestamp{ 144 | ID: os.Getpid() & 0xffff, 145 | Seq: 0, 146 | OriginTimestamp: 0, 147 | }, 148 | } 149 | 150 | data, err := m.Marshal(nil) 151 | if err != nil { 152 | return false 153 | } 154 | 155 | _, err = c.WriteTo(data, destAddr) 156 | if err != nil { 157 | return false 158 | } 159 | 160 | reply := make([]byte, 1500) 161 | err = c.SetReadDeadline(time.Now().Add(timeout)) 162 | if err != nil { 163 | return false 164 | } 165 | 166 | n, SourceIP, err := c.ReadFrom(reply) 167 | // timeout 168 | if err != nil { 169 | return false 170 | } 171 | // if anything is read from the connection it means that the host is alive 172 | if destAddr.String() == SourceIP.String() && n > 0 { 173 | return true 174 | } 175 | 176 | return false 177 | } 178 | 179 | // PingIcmpTimestampRequestAsync synchronous to the target ip address - ipv4 only 180 | func PingIcmpTimestampRequestAsync(ip string) { 181 | if !iputil.IsIPv4(ip) { 182 | return 183 | } 184 | if icmpConn4 == nil { 185 | return 186 | } 187 | destAddr := &net.IPAddr{IP: net.ParseIP(ip)} 188 | m := icmp.Message{ 189 | Type: ipv4.ICMPTypeTimestamp, 190 | Code: 0, 191 | Body: &Timestamp{ 192 | ID: os.Getpid() & 0xffff, 193 | Seq: 0, 194 | OriginTimestamp: 0, 195 | }, 196 | } 197 | 198 | data, err := m.Marshal(nil) 199 | if err != nil { 200 | return 201 | } 202 | 203 | _, err = icmpConn4.WriteTo(data, destAddr) 204 | if err != nil { 205 | return 206 | } 207 | } 208 | 209 | // Timestamp ICMP structure 210 | type Timestamp struct { 211 | ID int 212 | Seq int 213 | OriginTimestamp uint32 214 | ReceiveTimestamp uint32 215 | TransmitTimestamp uint32 216 | } 217 | 218 | const marshalledTimestampLen = 16 219 | 220 | // Len returns default timestamp length 221 | func (t *Timestamp) Len(_ int) int { 222 | if t == nil { 223 | return 0 224 | } 225 | return marshalledTimestampLen 226 | } 227 | 228 | // Marshal the timestamp structure 229 | func (t *Timestamp) Marshal(_ int) ([]byte, error) { 230 | bSize := marshalledTimestampLen / 2 231 | b := make([]byte, marshalledTimestampLen) 232 | b[0], b[1] = byte(t.ID>>bSize), byte(t.ID) 233 | b[2], b[3] = byte(t.Seq>>bSize), byte(t.Seq) 234 | 235 | unparseInt := func(i uint32) (byte, byte, byte, byte) { 236 | bs := make([]byte, 4) 237 | binary.LittleEndian.PutUint32(bs, i) 238 | return bs[3], bs[2], bs[1], bs[0] 239 | } 240 | 241 | b[4], b[5], b[6], b[7] = unparseInt(t.OriginTimestamp) 242 | b[8], b[9], b[10], b[11] = unparseInt(t.ReceiveTimestamp) 243 | b[12], b[13], b[14], b[15] = unparseInt(t.TransmitTimestamp) 244 | return b, nil 245 | } 246 | 247 | // ParseTimestamp to MessageBody structure 248 | func ParseTimestamp(_ int, b []byte) (icmp.MessageBody, error) { 249 | bodyLen := len(b) 250 | if bodyLen != marshalledTimestampLen { 251 | return nil, fmt.Errorf("timestamp body length %d not equal to 16", bodyLen) 252 | } 253 | p := &Timestamp{ID: int(b[0])<<8 | int(b[1]), Seq: int(b[2])<<8 | int(b[3])} 254 | 255 | parseInt := func(start int) uint32 { 256 | return uint32(b[start])<<24 | 257 | uint32(b[start+1])<<16 | 258 | uint32(b[start+2])<<8 | 259 | uint32(b[start+3]) 260 | } 261 | 262 | p.OriginTimestamp = parseInt(originTimestamp) 263 | p.ReceiveTimestamp = parseInt(receiveTimestamp) 264 | p.TransmitTimestamp = parseInt(transmitTimestamp) 265 | 266 | return p, nil 267 | } 268 | 269 | // PingIcmpAddressMaskRequestAsync asynchronous to the target ip address - ipv4 only 270 | func PingIcmpAddressMaskRequestAsync(ip string) { 271 | if !iputil.IsIPv4(ip) { 272 | return 273 | } 274 | if icmpConn4 == nil { 275 | return 276 | } 277 | destAddr := &net.IPAddr{IP: net.ParseIP(ip)} 278 | m := icmp.Message{ 279 | Type: ipv4.ICMPType(17), 280 | Code: 0, 281 | Body: &AddressMask{ 282 | ID: os.Getpid() & 0xffff, 283 | Seq: 0, 284 | AddressMask: 0, 285 | }, 286 | } 287 | 288 | data, err := m.Marshal(nil) 289 | if err != nil { 290 | return 291 | } 292 | retries := 0 293 | send: 294 | if retries >= maxRetries { 295 | return 296 | } 297 | _, err = icmpConn4.WriteTo(data, destAddr) 298 | if err != nil { 299 | retries++ 300 | // introduce a small delay to allow the network interface to flush the queue 301 | time.Sleep(time.Duration(DeadlineSec) * time.Millisecond) 302 | goto send 303 | } 304 | } 305 | 306 | // AddressMask ICMP structure 307 | type AddressMask struct { 308 | ID int 309 | Seq int 310 | AddressMask uint32 311 | } 312 | 313 | const marshalledAddressMaskLen = 8 314 | 315 | // Len returns default timestamp length 316 | func (a *AddressMask) Len(_ int) int { 317 | if a == nil { 318 | return 0 319 | } 320 | return marshalledAddressMaskLen 321 | } 322 | 323 | // Marshal the address mask structure 324 | func (a *AddressMask) Marshal(_ int) ([]byte, error) { 325 | bSize := marshalledAddressMaskLen / 2 326 | b := make([]byte, marshalledAddressMaskLen) 327 | b[0], b[1] = byte(a.ID>>bSize), byte(a.ID) 328 | b[2], b[3] = byte(a.Seq>>bSize), byte(a.Seq) 329 | 330 | unparseInt := func(i uint32) (byte, byte, byte, byte) { 331 | bs := make([]byte, 4) 332 | binary.LittleEndian.PutUint32(bs, i) 333 | return bs[3], bs[2], bs[1], bs[0] 334 | } 335 | 336 | b[4], b[5], b[6], b[7] = unparseInt(a.AddressMask) 337 | return b, nil 338 | } 339 | -------------------------------------------------------------------------------- /pkg/scan/ndp.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin 2 | 3 | package scan 4 | 5 | import ( 6 | "errors" 7 | "net" 8 | "os" 9 | "time" 10 | 11 | "github.com/projectdiscovery/gologger" 12 | "golang.org/x/net/icmp" 13 | "golang.org/x/net/ipv6" 14 | ) 15 | 16 | // PingNdpRequestAsync asynchronous to the target ip address 17 | func PingNdpRequestAsync(ip string) { 18 | networkInterface, _, _, err := PkgRouter.Route(net.ParseIP(ip)) 19 | if networkInterface == nil { 20 | err = errors.New("Could not send PingNdp Request packet to " + ip + ": no interface with outbound source found") 21 | } 22 | if err != nil { 23 | gologger.Debug().Msgf("%s\n", err) 24 | return 25 | } 26 | destAddr := &net.UDPAddr{IP: net.ParseIP(ip), Zone: networkInterface.Name} 27 | m := icmp.Message{ 28 | Type: ipv6.ICMPTypeEchoRequest, 29 | Code: 0, 30 | Body: &icmp.Echo{ 31 | ID: os.Getpid() & 0xffff, 32 | Seq: 1, 33 | Data: []byte(""), 34 | }, 35 | } 36 | 37 | data, err := m.Marshal(nil) 38 | if err != nil { 39 | return 40 | } 41 | retries := 0 42 | send: 43 | if retries >= maxRetries { 44 | return 45 | } 46 | _, err = icmpConn6.WriteTo(data, destAddr) 47 | if err != nil { 48 | retries++ 49 | // introduce a small delay to allow the network interface to flush the queue 50 | time.Sleep(time.Duration(DeadlineSec) * time.Millisecond) 51 | goto send 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/scan/option.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/projectdiscovery/naabu/v2/pkg/result" 7 | "github.com/projectdiscovery/networkpolicy" 8 | ) 9 | 10 | // Options of the scan 11 | type Options struct { 12 | Timeout time.Duration 13 | Retries int 14 | Rate int 15 | PortThreshold int 16 | ExcludeCdn bool 17 | OutputCdn bool 18 | ExcludedIps []string 19 | Proxy string 20 | ProxyAuth string 21 | Stream bool 22 | OnReceive result.ResultFn 23 | ScanType string 24 | NetworkPolicyOptions *networkpolicy.Options 25 | } 26 | -------------------------------------------------------------------------------- /pkg/scan/ping.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "os" 7 | "time" 8 | 9 | "golang.org/x/net/icmp" 10 | "golang.org/x/net/ipv4" 11 | ) 12 | 13 | // Some constants 14 | const ( 15 | DeadlineSec = 10 16 | ProtocolICMP = 1 17 | ProtocolIPv6ICMP = 58 18 | ) 19 | 20 | // PingResult contains the results for the Ping request 21 | type PingResult struct { 22 | Hosts []Ping 23 | } 24 | 25 | // Ping contains the results for ping on a single host 26 | type Ping struct { 27 | Type PingResultType 28 | Latency time.Duration 29 | Error error 30 | Host string 31 | } 32 | 33 | // PingResultType contains the type of result for ping request on an address 34 | type PingResultType int 35 | 36 | // Type of ping responses 37 | const ( 38 | HostInactive PingResultType = iota 39 | HostActive 40 | ) 41 | 42 | // PingHosts pings the addresses given and returns the latencies of each host 43 | // If the address returns an error, that address is marked as unusable. 44 | func PingHosts(addresses []string) (*PingResult, error) { 45 | // Start listening for icmp replies 46 | c, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") 47 | if err != nil { 48 | return nil, err 49 | } 50 | defer func() { 51 | _ = c.Close() 52 | }() 53 | 54 | results := &PingResult{Hosts: []Ping{}} 55 | var sequence int 56 | 57 | for _, addr := range addresses { 58 | // Resolve any DNS (if used) and get the real IP of the target 59 | dst, err := net.ResolveIPAddr("ip4", addr) 60 | if err != nil { 61 | results.Hosts = append(results.Hosts, Ping{Type: HostInactive, Error: err, Host: addr}) 62 | continue 63 | } 64 | 65 | sequence++ 66 | // Make a new ICMP message 67 | m := icmp.Message{ 68 | Type: ipv4.ICMPTypeEcho, 69 | Code: 0, 70 | Body: &icmp.Echo{ 71 | ID: os.Getpid() & 0xffff, 72 | Seq: sequence, 73 | Data: []byte(""), 74 | }, 75 | } 76 | 77 | data, err := m.Marshal(nil) 78 | if err != nil { 79 | results.Hosts = append(results.Hosts, Ping{Type: HostInactive, Error: err, Host: addr}) 80 | continue 81 | } 82 | 83 | // Send the packet 84 | start := time.Now() 85 | _, err = c.WriteTo(data, dst) 86 | if err != nil { 87 | results.Hosts = append(results.Hosts, Ping{Type: HostInactive, Error: err, Host: addr}) 88 | continue 89 | } 90 | 91 | reply := make([]byte, 1500) 92 | err = c.SetReadDeadline(time.Now().Add(DeadlineSec * time.Second)) 93 | if err != nil { 94 | results.Hosts = append(results.Hosts, Ping{Type: HostInactive, Error: err, Host: addr}) 95 | continue 96 | } 97 | 98 | n, _, err := c.ReadFrom(reply) 99 | if err != nil { 100 | results.Hosts = append(results.Hosts, Ping{Type: HostInactive, Error: err, Host: addr}) 101 | continue 102 | } 103 | duration := time.Since(start) 104 | 105 | rm, err := icmp.ParseMessage(ProtocolICMP, reply[:n]) 106 | if err != nil { 107 | results.Hosts = append(results.Hosts, Ping{Type: HostInactive, Error: err, Host: addr}) 108 | continue 109 | } 110 | switch rm.Type { 111 | case ipv4.ICMPTypeEchoReply: 112 | results.Hosts = append(results.Hosts, Ping{Type: HostActive, Latency: duration, Host: addr}) 113 | default: 114 | results.Hosts = append(results.Hosts, Ping{Type: HostInactive, Error: errors.New("no reply found for ping probe"), Host: addr}) 115 | continue 116 | } 117 | } 118 | 119 | return results, nil 120 | } 121 | 122 | // GetFastestHost gets the fastest host from the ping responses 123 | func (p *PingResult) GetFastestHost() (Ping, error) { 124 | var ping Ping 125 | 126 | // If the latency of the current host is less than the 127 | // host selected and host is active, use the host that has least latency. 128 | for _, host := range p.Hosts { 129 | if (host.Latency < ping.Latency || ping.Latency == 0) && host.Type == HostActive { 130 | ping.Type = HostActive 131 | ping.Latency = host.Latency 132 | ping.Host = host.Host 133 | } 134 | } 135 | 136 | if ping.Type != HostActive { 137 | return ping, errors.New("no active host found for target") 138 | } 139 | return ping, nil 140 | } 141 | -------------------------------------------------------------------------------- /pkg/scan/ping_test.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPingHosts(t *testing.T) { 11 | if os.Getuid() == 0 { 12 | tests := []struct { 13 | args string 14 | want bool 15 | wantErr bool 16 | }{ 17 | {"127.0.0.1", true, false}, 18 | {"localhost", true, false}, 19 | {"aaaaa", false, true}, 20 | } 21 | 22 | for _, tt := range tests { 23 | t.Run(tt.args, func(t *testing.T) { 24 | pingResults, err := PingHosts([]string{tt.args}) 25 | if tt.wantErr { 26 | assert.NotNil(t, err) 27 | } else { 28 | assert.Nil(t, err) 29 | } 30 | if tt.want { 31 | assert.NotEmpty(t, pingResults) 32 | fastest, err := pingResults.GetFastestHost() 33 | assert.Nil(t, err) 34 | assert.NotEmpty(t, pingResults.Hosts) 35 | assert.NotEmpty(t, fastest.Host) 36 | } else { 37 | assert.Empty(t, pingResults) 38 | } 39 | }) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/scan/scan.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "os" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/projectdiscovery/cdncheck" 15 | "github.com/projectdiscovery/gologger" 16 | "github.com/projectdiscovery/ipranger" 17 | "github.com/projectdiscovery/naabu/v2/pkg/port" 18 | "github.com/projectdiscovery/naabu/v2/pkg/protocol" 19 | "github.com/projectdiscovery/naabu/v2/pkg/result" 20 | "github.com/projectdiscovery/naabu/v2/pkg/utils/limits" 21 | "github.com/projectdiscovery/networkpolicy" 22 | "golang.org/x/net/proxy" 23 | ) 24 | 25 | // State determines the internal scan state 26 | type State int 27 | 28 | const ( 29 | maxRetries = 10 30 | sendDelayMsec = 10 31 | chanSize = 1000 //nolint 32 | packetSendSize = 2500 //nolint 33 | snaplen = 65536 //nolint 34 | readtimeout = 1500 //nolint 35 | ) 36 | 37 | const ( 38 | Init State = iota 39 | HostDiscovery 40 | Scan 41 | Done 42 | Guard 43 | ) 44 | 45 | type Phase struct { 46 | sync.RWMutex 47 | State 48 | } 49 | 50 | func (phase *Phase) Is(state State) bool { 51 | phase.RLock() 52 | defer phase.RUnlock() 53 | 54 | return phase.State == state 55 | } 56 | 57 | func (phase *Phase) Set(state State) { 58 | phase.Lock() 59 | defer phase.Unlock() 60 | 61 | phase.State = state 62 | } 63 | 64 | // PkgFlag represent the TCP packet flag 65 | type PkgFlag int 66 | 67 | const ( 68 | Syn PkgFlag = iota 69 | Ack 70 | IcmpEchoRequest 71 | IcmpTimestampRequest 72 | IcmpAddressMaskRequest 73 | Arp 74 | Ndp 75 | ) 76 | 77 | type Scanner struct { 78 | retries int 79 | rate int 80 | portThreshold int 81 | timeout time.Duration 82 | proxyDialer proxy.Dialer 83 | 84 | Ports []*port.Port 85 | IPRanger *ipranger.IPRanger 86 | 87 | HostDiscoveryResults *result.Result 88 | ScanResults *result.Result 89 | NetworkInterface *net.Interface 90 | cdn *cdncheck.Client 91 | tcpsequencer *TCPSequencer 92 | stream bool 93 | ListenHandler *ListenHandler 94 | OnReceive result.ResultFn 95 | } 96 | 97 | // PkgSend is a TCP package 98 | type PkgSend struct { 99 | ListenHandler *ListenHandler 100 | ip string 101 | port *port.Port 102 | flag PkgFlag 103 | SourceIP string 104 | } 105 | 106 | // PkgResult contains the results of sending TCP packages 107 | type PkgResult struct { 108 | ipv4 string 109 | ipv6 string 110 | port *port.Port 111 | } 112 | 113 | var ( 114 | pingIcmpEchoRequestCallback func(ip string, timeout time.Duration) bool //nolint 115 | pingIcmpTimestampRequestCallback func(ip string, timeout time.Duration) bool //nolint 116 | ) 117 | 118 | // NewScanner creates a new full port scanner that scans all ports using SYN packets. 119 | func NewScanner(options *Options) (*Scanner, error) { 120 | iprang, err := ipranger.New() 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | var nPolicyOptions *networkpolicy.Options 126 | if options.NetworkPolicyOptions != nil { 127 | nPolicyOptions = options.NetworkPolicyOptions 128 | } else { 129 | nPolicyOptions = &networkpolicy.Options{} 130 | } 131 | 132 | nPolicyOptions.DenyList = append(nPolicyOptions.DenyList, options.ExcludedIps...) 133 | nPolicy, err := networkpolicy.New(*nPolicyOptions) 134 | if err != nil { 135 | return nil, err 136 | } 137 | iprang.Np = nPolicy 138 | 139 | scanner := &Scanner{ 140 | timeout: options.Timeout, 141 | retries: options.Retries, 142 | rate: options.Rate, 143 | portThreshold: options.PortThreshold, 144 | tcpsequencer: NewTCPSequencer(), 145 | IPRanger: iprang, 146 | OnReceive: options.OnReceive, 147 | } 148 | 149 | scanner.HostDiscoveryResults = result.NewResult() 150 | scanner.ScanResults = result.NewResult() 151 | if options.ExcludeCdn || options.OutputCdn { 152 | scanner.cdn = cdncheck.New() 153 | } 154 | 155 | var auth *proxy.Auth = nil 156 | 157 | if options.ProxyAuth != "" && strings.Contains(options.ProxyAuth, ":") { 158 | credentials := strings.SplitN(options.ProxyAuth, ":", 2) 159 | var user, password string 160 | user = credentials[0] 161 | if len(credentials) == 2 { 162 | password = credentials[1] 163 | } 164 | auth = &proxy.Auth{User: user, Password: password} 165 | } 166 | 167 | if options.Proxy != "" { 168 | proxyDialer, err := proxy.SOCKS5("tcp", options.Proxy, auth, &net.Dialer{Timeout: limits.TimeoutWithProxy(options.Timeout)}) 169 | if err != nil { 170 | return nil, err 171 | } 172 | scanner.proxyDialer = proxyDialer 173 | } 174 | 175 | scanner.stream = options.Stream 176 | acquire: 177 | if handler, err := Acquire(options); err != nil { 178 | // automatically fallback to connect scan 179 | if options.ScanType == "s" { 180 | gologger.Info().Msgf("syn scan is not possible, falling back to connect scan") 181 | options.ScanType = "c" 182 | goto acquire 183 | } 184 | return scanner, err 185 | } else { 186 | scanner.ListenHandler = handler 187 | } 188 | 189 | return scanner, err 190 | } 191 | 192 | // Close the scanner and terminate all workers 193 | func (s *Scanner) Close() error { 194 | s.ListenHandler.Busy = false 195 | s.ListenHandler = nil 196 | 197 | return nil 198 | } 199 | 200 | // StartWorkers of the scanner 201 | func (s *Scanner) StartWorkers(ctx context.Context) { 202 | go s.ICMPResultWorker(ctx) 203 | go s.TCPResultWorker(ctx) 204 | go s.UDPResultWorker(ctx) 205 | } 206 | 207 | // EnqueueICMP outgoing ICMP packets 208 | func (s *Scanner) EnqueueICMP(ip string, pkgtype PkgFlag) { 209 | icmpPacketSend <- &PkgSend{ 210 | ListenHandler: s.ListenHandler, 211 | ip: ip, 212 | flag: pkgtype, 213 | } 214 | } 215 | 216 | // EnqueueEthernet outgoing Ethernet packets 217 | func (s *Scanner) EnqueueEthernet(ip string, pkgtype PkgFlag) { 218 | ethernetPacketSend <- &PkgSend{ 219 | ListenHandler: s.ListenHandler, 220 | ip: ip, 221 | flag: pkgtype, 222 | } 223 | } 224 | 225 | // EnqueueTCP outgoing TCP packets 226 | func (s *Scanner) EnqueueTCP(ip string, pkgtype PkgFlag, ports ...*port.Port) { 227 | for _, port := range ports { 228 | transportPacketSend <- &PkgSend{ 229 | ListenHandler: s.ListenHandler, 230 | ip: ip, 231 | port: port, 232 | flag: pkgtype, 233 | } 234 | } 235 | } 236 | 237 | // EnqueueTCP outgoing TCP packets 238 | func (s *Scanner) EnqueueUDP(ip string, ports ...*port.Port) { 239 | for _, port := range ports { 240 | transportPacketSend <- &PkgSend{ 241 | ListenHandler: s.ListenHandler, 242 | ip: ip, 243 | port: port, 244 | } 245 | } 246 | } 247 | 248 | // ICMPResultWorker handles ICMP responses (used only during probes) 249 | func (s *Scanner) ICMPResultWorker(ctx context.Context) { 250 | for { 251 | select { 252 | case <-ctx.Done(): 253 | return 254 | case ip := <-s.ListenHandler.HostDiscoveryChan: 255 | if s.ListenHandler.Phase.Is(HostDiscovery) { 256 | gologger.Debug().Msgf("Received ICMP response from %s\n", ip.ipv4) 257 | if ip.ipv4 != "" { 258 | s.HostDiscoveryResults.AddIp(ip.ipv4) 259 | } 260 | if ip.ipv6 != "" { 261 | s.HostDiscoveryResults.AddIp(ip.ipv6) 262 | } 263 | } 264 | } 265 | } 266 | } 267 | 268 | // TCPResultWorker handles probes and scan results 269 | func (s *Scanner) TCPResultWorker(ctx context.Context) { 270 | for { 271 | select { 272 | case <-ctx.Done(): 273 | return 274 | case ip := <-s.ListenHandler.TcpChan: 275 | srcIP4WithPort := net.JoinHostPort(ip.ipv4, ip.port.String()) 276 | srcIP6WithPort := net.JoinHostPort(ip.ipv6, ip.port.String()) 277 | isIPInRange := s.IPRanger.ContainsAny(srcIP4WithPort, srcIP6WithPort, ip.ipv4, ip.ipv6) 278 | if !isIPInRange { 279 | gologger.Debug().Msgf("Discarding Transport packet from non target ips: ip4=%s ip6=%s\n", ip.ipv4, ip.ipv6) 280 | continue 281 | } 282 | 283 | if s.OnReceive != nil { 284 | singlePort := []*port.Port{ip.port} 285 | if ip.ipv4 != "" { 286 | s.OnReceive(&result.HostResult{IP: ip.ipv4, Ports: singlePort}) 287 | } 288 | if ip.ipv6 != "" { 289 | s.OnReceive(&result.HostResult{IP: ip.ipv6, Ports: singlePort}) 290 | } 291 | } 292 | if s.ListenHandler.Phase.Is(HostDiscovery) { 293 | gologger.Debug().Msgf("Received Transport (TCP|UDP) probe response from ipv4:%s ipv6:%s port:%d\n", ip.ipv4, ip.ipv6, ip.port.Port) 294 | if ip.ipv4 != "" { 295 | s.HostDiscoveryResults.AddIp(ip.ipv4) 296 | } 297 | if ip.ipv6 != "" { 298 | s.HostDiscoveryResults.AddIp(ip.ipv6) 299 | } 300 | } else if s.ListenHandler.Phase.Is(Scan) || s.stream { 301 | gologger.Debug().Msgf("Received Transport (TCP) scan response from ipv4:%s ipv6:%s port:%d\n", ip.ipv4, ip.ipv6, ip.port.Port) 302 | if ip.ipv4 != "" { 303 | s.ScanResults.AddPort(ip.ipv4, ip.port) 304 | } 305 | if ip.ipv6 != "" { 306 | s.ScanResults.AddPort(ip.ipv6, ip.port) 307 | } 308 | } 309 | } 310 | } 311 | } 312 | 313 | // UDPResultWorker handles probes and scan results 314 | func (s *Scanner) UDPResultWorker(ctx context.Context) { 315 | for { 316 | select { 317 | case <-ctx.Done(): 318 | return 319 | case ip := <-s.ListenHandler.UdpChan: 320 | srcIP4WithPort := net.JoinHostPort(ip.ipv4, ip.port.String()) 321 | srcIP6WithPort := net.JoinHostPort(ip.ipv6, ip.port.String()) 322 | isIPInRange := s.IPRanger.ContainsAny(srcIP4WithPort, srcIP6WithPort, ip.ipv4, ip.ipv6) 323 | if !isIPInRange { 324 | gologger.Debug().Msgf("Discarding Transport packet from non target ips: ip4=%s ip6=%s\n", ip.ipv4, ip.ipv6) 325 | continue 326 | } 327 | 328 | if s.ListenHandler.Phase.Is(HostDiscovery) { 329 | gologger.Debug().Msgf("Received UDP probe response from ipv4:%s ipv6:%s port:%d\n", ip.ipv4, ip.ipv6, ip.port.Port) 330 | if ip.ipv4 != "" { 331 | s.HostDiscoveryResults.AddIp(ip.ipv4) 332 | } 333 | if ip.ipv6 != "" { 334 | s.HostDiscoveryResults.AddIp(ip.ipv6) 335 | } 336 | } else if s.ListenHandler.Phase.Is(Scan) || s.stream { 337 | gologger.Debug().Msgf("Received Transport (UDP) scan response from from ipv4:%s ipv6:%s port:%d\n", ip.ipv4, ip.ipv6, ip.port.Port) 338 | if ip.ipv4 != "" { 339 | s.ScanResults.AddPort(ip.ipv4, ip.port) 340 | } 341 | if ip.ipv6 != "" { 342 | s.ScanResults.AddPort(ip.ipv6, ip.port) 343 | } 344 | } 345 | } 346 | } 347 | } 348 | 349 | // ScanSyn a target ip 350 | func (s *Scanner) ScanSyn(ip string) { 351 | for _, port := range s.Ports { 352 | s.EnqueueTCP(ip, Syn, port) 353 | } 354 | } 355 | 356 | // GetInterfaceFromIP gets the name of the network interface from local ip address 357 | func GetInterfaceFromIP(ip net.IP) (*net.Interface, error) { 358 | address := ip.String() 359 | 360 | interfaces, err := net.Interfaces() 361 | if err != nil { 362 | return nil, err 363 | } 364 | 365 | for _, i := range interfaces { 366 | byNameInterface, err := net.InterfaceByName(i.Name) 367 | if err != nil { 368 | return nil, err 369 | } 370 | 371 | addresses, err := byNameInterface.Addrs() 372 | if err != nil { 373 | return nil, err 374 | } 375 | 376 | for _, v := range addresses { 377 | // Check if the IP for the current interface is our 378 | // source IP. If yes, return the interface 379 | if strings.HasPrefix(v.String(), address+"/") { 380 | return byNameInterface, nil 381 | } 382 | } 383 | } 384 | 385 | return nil, fmt.Errorf("no interface found for ip %s", address) 386 | } 387 | 388 | // ConnectPort a single host and port 389 | func (s *Scanner) ConnectPort(host string, p *port.Port, timeout time.Duration) (bool, error) { 390 | hostport := net.JoinHostPort(host, fmt.Sprint(p.Port)) 391 | var ( 392 | err error 393 | conn net.Conn 394 | ) 395 | if s.proxyDialer != nil { 396 | ctx, cancel := context.WithTimeout(context.Background(), limits.TimeoutWithProxy(timeout)) 397 | defer cancel() 398 | proxyDialer, ok := s.proxyDialer.(proxy.ContextDialer) 399 | if !ok { 400 | return false, errors.New("invalid proxy dialer") 401 | } 402 | conn, err = proxyDialer.DialContext(ctx, p.Protocol.String(), hostport) 403 | if err != nil { 404 | return false, err 405 | } 406 | } else { 407 | netDialer := net.Dialer{ 408 | Timeout: timeout, 409 | } 410 | if s.ListenHandler.SourceIp4 != nil { 411 | netDialer.LocalAddr = &net.TCPAddr{IP: s.ListenHandler.SourceIp4} 412 | } else if s.ListenHandler.SourceIP6 != nil { 413 | netDialer.LocalAddr = &net.TCPAddr{IP: s.ListenHandler.SourceIP6} 414 | } 415 | conn, err = netDialer.Dial(p.Protocol.String(), hostport) 416 | } 417 | if err != nil { 418 | return false, err 419 | } 420 | defer func() { 421 | _ = conn.Close() 422 | }() 423 | 424 | // udp needs data probe 425 | switch p.Protocol { 426 | case protocol.UDP: 427 | if err := conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil { 428 | return false, err 429 | } 430 | if _, err := conn.Write(nil); err != nil { 431 | return false, err 432 | } 433 | if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { 434 | return false, err 435 | } 436 | n, err := io.Copy(io.Discard, conn) 437 | // ignore timeout errors 438 | if err != nil && !os.IsTimeout(err) { 439 | return false, err 440 | } 441 | return n > 0, nil 442 | } 443 | 444 | return true, err 445 | } 446 | -------------------------------------------------------------------------------- /pkg/scan/scan_common.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | 7 | "github.com/projectdiscovery/gologger" 8 | "github.com/projectdiscovery/naabu/v2/pkg/privileges" 9 | "github.com/projectdiscovery/naabu/v2/pkg/routing" 10 | "golang.org/x/net/icmp" 11 | ) 12 | 13 | const ( 14 | IPv4 = "4" 15 | IPv6 = "6" 16 | ) 17 | 18 | var ( 19 | ListenHandlers []*ListenHandler 20 | NetworkInterface string 21 | networkInterface *net.Interface 22 | transportPacketSend, icmpPacketSend, ethernetPacketSend chan *PkgSend 23 | icmpConn4, icmpConn6 *icmp.PacketConn 24 | 25 | PkgRouter routing.Router 26 | 27 | ArpRequestAsync func(ip string) 28 | InitScanner func(s *Scanner) error 29 | NumberOfHandlers = 1 30 | tcpsequencer = NewTCPSequencer() 31 | ) 32 | 33 | type ListenHandler struct { 34 | Busy bool 35 | Phase *Phase 36 | SourceHW net.HardwareAddr 37 | SourceIp4 net.IP 38 | SourceIP6 net.IP 39 | Port int 40 | TcpConn4, UdpConn4, TcpConn6, UdpConn6 *net.IPConn 41 | TcpChan, UdpChan, HostDiscoveryChan chan *PkgResult 42 | } 43 | 44 | func NewListenHandler() *ListenHandler { 45 | return &ListenHandler{Phase: &Phase{}} 46 | } 47 | 48 | func Acquire(options *Options) (*ListenHandler, error) { 49 | // always grant to unprivileged scans or connect scan 50 | if PkgRouter == nil || !privileges.IsPrivileged || options.ScanType == "c" { 51 | h := NewListenHandler() 52 | h.Busy = true 53 | return NewListenHandler(), nil 54 | } 55 | 56 | for _, listenHandler := range ListenHandlers { 57 | if !listenHandler.Busy { 58 | listenHandler.Phase = &Phase{} 59 | listenHandler.Busy = true 60 | return listenHandler, nil 61 | } 62 | } 63 | return nil, errors.New("no free handlers") 64 | } 65 | 66 | func (l *ListenHandler) Release() { 67 | l.Busy = false 68 | l.Phase = nil 69 | } 70 | 71 | func init() { 72 | if r, err := routing.New(); err != nil { 73 | gologger.Error().Msgf("could not initialize router: %s\n", err) 74 | } else { 75 | PkgRouter = r 76 | } 77 | } 78 | 79 | func ToString(ip net.IP) string { 80 | if len(ip) == 0 { 81 | return "" 82 | } 83 | return ip.String() 84 | } 85 | -------------------------------------------------------------------------------- /pkg/scan/scan_win.go: -------------------------------------------------------------------------------- 1 | //go:build windowz 2 | 3 | package scan 4 | -------------------------------------------------------------------------------- /pkg/scan/tcpsequencer.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "math" 5 | "sync/atomic" 6 | ) 7 | 8 | // TCPSequencer generates linear TCP sequence numbers that wrap 9 | // around after reaching their maximum value. 10 | // 11 | // According to specs, this is the correct way to approach TCP sequence 12 | // number since linearity will be guaranteed by the wrapping around to initial 0. 13 | type TCPSequencer struct { 14 | current uint32 15 | } 16 | 17 | // NewTCPSequencer creates a new linear tcp sequenc enumber generator 18 | func NewTCPSequencer() *TCPSequencer { 19 | // Start the sequence with math.MaxUint32, which will then wrap around 20 | // when incremented starting the sequence with 0 as desired. 21 | return &TCPSequencer{current: math.MaxUint32} 22 | } 23 | 24 | // Next returns the next number in the sequence of tcp sequence numbers 25 | func (t *TCPSequencer) Next() uint32 { 26 | value := atomic.AddUint32(&t.current, 1) 27 | return value 28 | } 29 | -------------------------------------------------------------------------------- /pkg/scan/tcpsequencer_test.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTCPSequencer(t *testing.T) { 10 | tcpSequencer := NewTCPSequencer() 11 | // tcp sequencer should be uint32 incremental 12 | for i := 0; i < 50000; i++ { 13 | actual := tcpSequencer.Next() 14 | assert.Equal(t, uint32(i), actual) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pkg/utils/limits/rate.go: -------------------------------------------------------------------------------- 1 | package limits 2 | 3 | func RateLimitWithProxy(rateLimit int) int { 4 | return rateLimit / 2 5 | } 6 | -------------------------------------------------------------------------------- /pkg/utils/limits/timeout.go: -------------------------------------------------------------------------------- 1 | package limits 2 | 3 | import "time" 4 | 5 | func TimeoutWithProxy(timeout time.Duration) time.Duration { 6 | return timeout * 2 7 | } 8 | -------------------------------------------------------------------------------- /static/naabu-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectdiscovery/naabu/baf390be60d6ebef43352fafacdb4fa3bb8a27c4/static/naabu-logo.png -------------------------------------------------------------------------------- /static/naabu-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectdiscovery/naabu/baf390be60d6ebef43352fafacdb4fa3bb8a27c4/static/naabu-run.png --------------------------------------------------------------------------------