├── .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 |
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 |
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
--------------------------------------------------------------------------------