├── wappalyzer ├── .gitignore ├── .github │ ├── workflows │ │ ├── build-test.yml │ │ ├── lint-test.yml │ │ ├── codeql-analysis.yml │ │ ├── autorelease-tag.yml │ │ └── fingerprint-update.yml │ └── dependabot.yml ├── fingerprints_test.go ├── examples │ └── main.go ├── LICENSE.md ├── README.md ├── fingerprint_headers.go ├── fingerprint_cookies.go ├── wappalyzergo_test.go ├── fingerprint_body.go ├── wappalyzer.go └── cmd │ └── update-fingerprints │ └── main.go ├── resources ├── ehole.json.gz ├── goby.json.gz ├── wappalyzer.json.gz ├── fingers_http.json.gz ├── fingers_socket.json.gz ├── nmap-services.json.gz ├── fingerprinthub_web.json.gz ├── nmap-service-probes.json.gz ├── fingerprinthub_service.json.gz ├── embed_test.go ├── noembed.go ├── utils.go ├── loader.go ├── embed.go ├── fingerprinthub_v4.py └── aliases.yaml ├── fingers ├── types.go ├── matcher.go ├── common.go ├── rules.go ├── engine.go └── fingers.go ├── nmap ├── type-probelist.go ├── gonmap_test.go ├── type-response.go ├── nmap-customize-probes.go ├── type-portlist.go ├── init_data.go ├── nmap-services.go ├── data.go ├── type-fingerprint.go ├── engine.go ├── type-probe.go └── type-match.go ├── go.mod ├── doc ├── README.md └── rule.md ├── favicon └── favicon.go ├── cmd ├── README.md ├── engine │ ├── README.md │ └── example.go └── nmap │ ├── README.md │ └── nmap.go ├── common ├── attributes.go ├── vuln.go ├── sender.go └── framework.go ├── goby └── goby.go ├── ehole └── ehole.go ├── README.md ├── alias └── alias.go ├── fingerprinthub ├── active_match_test.go ├── README.md └── fingerprinthub_test.go └── engine_test.go /wappalyzer/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | cmd/update-fingerprints/update-fingerprints 3 | -------------------------------------------------------------------------------- /resources/ehole.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainreactors/fingers/HEAD/resources/ehole.json.gz -------------------------------------------------------------------------------- /resources/goby.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainreactors/fingers/HEAD/resources/goby.json.gz -------------------------------------------------------------------------------- /resources/wappalyzer.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainreactors/fingers/HEAD/resources/wappalyzer.json.gz -------------------------------------------------------------------------------- /resources/fingers_http.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainreactors/fingers/HEAD/resources/fingers_http.json.gz -------------------------------------------------------------------------------- /resources/fingers_socket.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainreactors/fingers/HEAD/resources/fingers_socket.json.gz -------------------------------------------------------------------------------- /resources/nmap-services.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainreactors/fingers/HEAD/resources/nmap-services.json.gz -------------------------------------------------------------------------------- /resources/fingerprinthub_web.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainreactors/fingers/HEAD/resources/fingerprinthub_web.json.gz -------------------------------------------------------------------------------- /resources/nmap-service-probes.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainreactors/fingers/HEAD/resources/nmap-service-probes.json.gz -------------------------------------------------------------------------------- /resources/fingerprinthub_service.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainreactors/fingers/HEAD/resources/fingerprinthub_service.json.gz -------------------------------------------------------------------------------- /resources/embed_test.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | "github.com/chainreactors/utils/encode" 6 | "testing" 7 | ) 8 | 9 | func TestGzipDecompress(t *testing.T) { 10 | t.Log("TestGzipDecompress") 11 | decompress := encode.MustGzipDecompress(EholeData) 12 | fmt.Println(string(decompress)) 13 | } 14 | -------------------------------------------------------------------------------- /wappalyzer/.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: 🔨 Build Test 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | 8 | jobs: 9 | build: 10 | name: Test Builds 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Set up Go 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version: 1.21.x 17 | 18 | - name: Check out code 19 | uses: actions/checkout@v4 20 | 21 | - name: Test 22 | run: go test ./... -------------------------------------------------------------------------------- /fingers/types.go: -------------------------------------------------------------------------------- 1 | package fingers 2 | 3 | import "github.com/chainreactors/fingers/common" 4 | 5 | const ( 6 | None = iota 7 | ACTIVE 8 | ICO 9 | NOTFOUND 10 | GUESS 11 | ) 12 | 13 | const ( 14 | INFO int = iota + 1 15 | MEDIUM 16 | HIGH 17 | CRITICAL 18 | ) 19 | 20 | type Sender func([]byte) ([]byte, bool) 21 | type Callback func(*common.Framework, *common.Vuln) 22 | type senddata []byte 23 | 24 | func (d senddata) IsNull() bool { 25 | if len(d) == 0 { 26 | return true 27 | } 28 | return false 29 | } 30 | -------------------------------------------------------------------------------- /wappalyzer/.github/workflows/lint-test.yml: -------------------------------------------------------------------------------- 1 | name: 🙏🏻 Lint Test 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | lint: 9 | name: Lint Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | - name: Set up Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: 1.21.x 18 | - name: Run golangci-lint 19 | uses: golangci/golangci-lint-action@v4.0.0 20 | with: 21 | version: latest 22 | args: --timeout 5m 23 | working-directory: . 24 | -------------------------------------------------------------------------------- /nmap/type-probelist.go: -------------------------------------------------------------------------------- 1 | package gonmap 2 | 3 | type ProbeList []string 4 | 5 | var emptyProbeList []string 6 | 7 | func (p ProbeList) removeDuplicate() ProbeList { 8 | result := make([]string, 0, len(p)) 9 | temp := map[string]struct{}{} 10 | for _, item := range p { 11 | if _, ok := temp[item]; !ok { //如果字典中找不到元素,ok=false,!ok为true,就往切片中append元素。 12 | temp[item] = struct{}{} 13 | result = append(result, item) 14 | } 15 | } 16 | return result 17 | } 18 | 19 | func (p ProbeList) exist(probeName string) bool { 20 | for _, name := range p { 21 | if name == probeName { 22 | return true 23 | } 24 | } 25 | return false 26 | } 27 | -------------------------------------------------------------------------------- /nmap/gonmap_test.go: -------------------------------------------------------------------------------- 1 | package gonmap 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestScan(t *testing.T) { 10 | var scanner = New() 11 | host := "127.0.0.1" // 使用本地地址避免网络问题 12 | 13 | testPorts := []int{22, 80, 138, 135, 3389, 1080, 443, 445, 1521} 14 | 15 | for _, port := range testPorts { 16 | status, response := scanner.ScanTimeout(host, port, time.Second*3) 17 | if response != nil && response.FingerPrint != nil { 18 | fmt.Printf("Port %d: %s - %s\n", port, status, response.FingerPrint.Service) 19 | } else { 20 | fmt.Printf("Port %d: %s - no service detected\n", port, status) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /wappalyzer/fingerprints_test.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestVersionRegex(t *testing.T) { 10 | regex, err := newVersionRegex("JBoss(?:-([\\d.]+))?\\;confidence:50\\;version:\\1") 11 | require.NoError(t, err, "could not create version regex") 12 | 13 | matched, version := regex.MatchString("JBoss-2.3.9") 14 | require.True(t, matched, "could not get version regex match") 15 | require.Equal(t, "2.3.9", version, "could not get correct version") 16 | 17 | t.Run("confidence-only", func(t *testing.T) { 18 | _, err := newVersionRegex("\\;confidence:50") 19 | require.NoError(t, err, "could create invalid version regex") 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /nmap/type-response.go: -------------------------------------------------------------------------------- 1 | package gonmap 2 | 3 | const ( 4 | Closed Status = 0x000a1 5 | Open = 0x000b2 6 | Matched = 0x000c3 7 | NotMatched = 0x000d4 8 | Unknown = 0x000e5 9 | ) 10 | 11 | type Status int 12 | 13 | func (s Status) String() string { 14 | switch s { 15 | case Closed: 16 | return "Closed" 17 | case Open: 18 | return "Open" 19 | case Matched: 20 | return "Matched" 21 | case NotMatched: 22 | return "NotMatched" 23 | case Unknown: 24 | return "Unknown" 25 | default: 26 | return "Unknown" 27 | } 28 | } 29 | 30 | type Response struct { 31 | Raw string 32 | TLS bool 33 | FingerPrint *FingerPrint 34 | } 35 | 36 | var dnsResponse = Response{Raw: "DnsServer", TLS: false, 37 | FingerPrint: &FingerPrint{ 38 | Service: "dns", 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /resources/noembed.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.16 || noembed 2 | // +build !go1.16 noembed 3 | 4 | package resources 5 | 6 | import ( 7 | "encoding/json" 8 | "github.com/chainreactors/utils" 9 | ) 10 | 11 | var AliasesData []byte 12 | 13 | var PortData []byte // json format 14 | var PrePort *utils.PortPreset 15 | 16 | func LoadPorts() (*utils.PortPreset, error) { 17 | var ports []*utils.PortConfig 18 | var err error 19 | err = json.Unmarshal(PortData, &ports) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | PrePort = utils.NewPortPreset(ports) 25 | return PrePort, nil 26 | } 27 | 28 | // engine 29 | var FingersHTTPData []byte 30 | var FingersSocketData []byte 31 | 32 | var GobyData []byte 33 | var FingerprinthubWebData []byte 34 | var FingerprinthubServiceData []byte 35 | var EholeData []byte 36 | var WappalyzerData []byte 37 | 38 | var CheckSum = map[string]string{} 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chainreactors/fingers 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/chainreactors/logs v0.0.0-20241030063019-8ca66a3ee307 7 | github.com/chainreactors/utils v0.0.0-20250831165528-f06246b0f311 8 | github.com/stretchr/testify v1.9.0 9 | golang.org/x/net v0.21.0 10 | ) 11 | 12 | require ( 13 | github.com/chainreactors/files v0.0.0-20240716182835-7884ee1e77f0 // indirect 14 | github.com/chainreactors/neutron v0.0.0-20251216154716-7c28cb6fdf03 15 | github.com/chainreactors/words v0.0.0-20241002061906-25d8893158d9 16 | github.com/dlclark/regexp2 v1.11.5 17 | github.com/facebookincubator/nvdtools v0.1.5 18 | github.com/invopop/jsonschema v0.13.0 19 | github.com/jessevdk/go-flags v1.6.1 20 | github.com/mozillazg/go-pinyin v0.20.0 21 | github.com/pkg/errors v0.9.1 22 | gopkg.in/yaml.v3 v3.0.1 23 | ) 24 | 25 | replace github.com/chainreactors/neutron => ../neutron 26 | -------------------------------------------------------------------------------- /wappalyzer/examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/chainreactors/fingers/resources" 10 | "github.com/chainreactors/fingers/wappalyzer" 11 | ) 12 | 13 | func main() { 14 | resp, err := http.DefaultClient.Get("https://www.hackerone.com") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | data, _ := io.ReadAll(resp.Body) // Ignoring error for example 19 | 20 | wappalyzerClient, err := wappalyzer.NewWappalyzeEngine(resources.WappalyzerData) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | fingerprints := wappalyzerClient.Fingerprint(resp.Header, data) 25 | fmt.Printf("%v\n", fingerprints) 26 | // Output: map[Acquia Cloud Platform:{} Amazon EC2:{} Apache:{} Cloudflare:{} Drupal:{} PHP:{} Percona:{} React:{} Varnish:{}] 27 | 28 | fingerprintsWithCats := wappalyzerClient.FingerprintWithCats(resp.Header, data) 29 | fmt.Printf("%v\n", fingerprintsWithCats) 30 | } 31 | -------------------------------------------------------------------------------- /wappalyzer/.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 🚨 CodeQL Analysis 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - dev 8 | 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: Checkout repository 26 | uses: actions/checkout@v4 27 | 28 | # Initializes the CodeQL tools for scanning. 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | - name: Autobuild 35 | uses: github/codeql-action/autobuild@v3 36 | 37 | - name: Perform CodeQL Analysis 38 | uses: github/codeql-action/analyze@v3 -------------------------------------------------------------------------------- /wappalyzer/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 | -------------------------------------------------------------------------------- /wappalyzer/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | # Maintain dependencies for GitHub Actions 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | target-branch: "main" 15 | commit-message: 16 | prefix: "chore" 17 | include: "scope" 18 | 19 | # Maintain dependencies for go modules 20 | - package-ecosystem: "gomod" 21 | directory: "/" 22 | schedule: 23 | interval: "weekly" 24 | target-branch: "main" 25 | commit-message: 26 | prefix: "chore" 27 | include: "scope" 28 | 29 | # Maintain dependencies for docker 30 | - package-ecosystem: "docker" 31 | directory: "/" 32 | schedule: 33 | interval: "weekly" 34 | target-branch: "main" 35 | commit-message: 36 | prefix: "chore" 37 | include: "scope" 38 | -------------------------------------------------------------------------------- /nmap/nmap-customize-probes.go: -------------------------------------------------------------------------------- 1 | package gonmap 2 | 3 | var nmapCustomizeProbes = ` 4 | Probe TCP SMB_NEGOTIATE q|\x00\x00\x00\xc0\xfeSMB@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00\b\x00\x01\x00\x00\x00\u007f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00x\x00\x00\x00\x02\x00\x00\x00\x02\x02\x10\x02"\x02$\x02\x00\x03\x02\x03\x10\x03\x11\x03\x00\x00\x00\x00\x01\x00&\x00\x00\x00\x00\x00\x01\x00 \x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00| 5 | rarity 1 6 | ports 445 7 | 8 | match microsoft-ds m|^\0\0...SMB.*|s 9 | 10 | Probe TCP JSON_RPC q|{"id":1,"jsonrpc":"2.0","method":"login","params":{}}\r\n| 11 | rarity 4 12 | ports 443,80,8443,8080 13 | 14 | match jsonrpc m|^{"jsonrpc":"([\d.]+)".*"height":(\d+),"seed_hash".*|s v/$1/ p/ETH/ i/height:$2/ 15 | match jsonrpc m|^{"jsonrpc":"([\d.]+)".*|s v/$1/ 16 | 17 | ` 18 | -------------------------------------------------------------------------------- /wappalyzer/.github/workflows/autorelease-tag.yml: -------------------------------------------------------------------------------- 1 | name: 🔖 Release Tag 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["💡Fingerprints Update"] 6 | types: 7 | - completed 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Get Commit Count 19 | id: get_commit 20 | run: git rev-list `git rev-list --tags --no-walk --max-count=1`..HEAD --count | xargs -I {} echo COMMIT_COUNT={} >> $GITHUB_OUTPUT 21 | 22 | - name: Create release and tag 23 | if: ${{ steps.get_commit.outputs.COMMIT_COUNT > 0 }} 24 | id: tag_version 25 | uses: mathieudutour/github-tag-action@v6.1 26 | with: 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - name: Create a GitHub release 30 | if: ${{ steps.get_commit.outputs.COMMIT_COUNT > 0 }} 31 | uses: actions/create-release@v1 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | with: 35 | tag_name: ${{ steps.tag_version.outputs.new_tag }} 36 | release_name: Release ${{ steps.tag_version.outputs.new_tag }} 37 | body: ${{ steps.tag_version.outputs.changelog }} -------------------------------------------------------------------------------- /wappalyzer/.github/workflows/fingerprint-update.yml: -------------------------------------------------------------------------------- 1 | name: 💡Fingerprints Update 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * 0' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Repo 13 | uses: actions/checkout@v4 14 | with: 15 | persist-credentials: false 16 | 17 | - name: Setup golang 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: 1.21.x 21 | 22 | - name: Installing Update binary 23 | run: | 24 | go install github.com/projectdiscovery/wappalyzergo/cmd/update-fingerprints 25 | shell: bash 26 | 27 | - name: Downloading latest wappalyzer changes 28 | run: | 29 | update-fingerprints -fingerprints fingerprints_data.json 30 | shell: bash 31 | 32 | - name: Create local changes 33 | run: | 34 | git add fingerprints_data.json 35 | 36 | - name: Commit files 37 | run: | 38 | git config --local user.email "action@github.com" 39 | git config --local user.name "GitHub Action" 40 | git commit -m "Weekly fingerprints update [$(date)] :robot:" -a --allow-empty 41 | 42 | - name: Push changes 43 | uses: ad-m/github-push-action@master 44 | with: 45 | github_token: ${{ secrets.GITHUB_TOKEN }} 46 | branch: ${{ github.ref }} -------------------------------------------------------------------------------- /resources/utils.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/chainreactors/utils/encode" 7 | "github.com/mozillazg/go-pinyin" 8 | "strings" 9 | ) 10 | 11 | var pinyinArgs = pinyin.NewArgs() 12 | 13 | func UnmarshalData(data []byte, v interface{}) error { 14 | var err error 15 | if bytes.HasPrefix(data, []byte{0x1f, 0x8b}) { 16 | data, err = encode.GzipDecompress(data) 17 | if err != nil { 18 | return nil 19 | } 20 | } 21 | 22 | return json.Unmarshal(data, &v) 23 | } 24 | 25 | // ConvertChineseToPinyin converts Chinese characters to Pinyin. 26 | func ConvertChineseToPinyin(input string) string { 27 | var s strings.Builder 28 | for _, i := range input { 29 | if i >= 0x4e00 && i <= 0x9fa5 { 30 | if py := pinyin.SinglePinyin(i, pinyinArgs); len(py) > 0 { 31 | s.WriteString(py[0]) 32 | } else { 33 | s.WriteRune(i) 34 | } 35 | } else { 36 | s.WriteRune(i) 37 | } 38 | } 39 | return s.String() 40 | } 41 | 42 | // NormalizeString performs normalization on the input string. 43 | func NormalizeString(s string) string { 44 | // Convert Chinese to Pinyin 45 | s = ConvertChineseToPinyin(s) 46 | 47 | // Convert to lower case 48 | s = strings.ToLower(s) 49 | 50 | // Replace '-' with '_' 51 | s = strings.Replace(s, "-", "", -1) 52 | 53 | s = strings.Replace(s, "_", "", -1) 54 | 55 | // Remove spaces 56 | s = strings.Replace(s, " ", "", -1) 57 | 58 | return s 59 | } 60 | -------------------------------------------------------------------------------- /wappalyzer/README.md: -------------------------------------------------------------------------------- 1 | # Wappalyzergo 2 | 3 | A high performance port of the Wappalyzer Technology Detection Library to Go. Inspired by [Webanalyze](https://github.com/rverton/webanalyze). 4 | 5 | Uses data from https://github.com/AliasIO/wappalyzer 6 | 7 | ## Features 8 | 9 | - Very simple and easy to use, with clean codebase. 10 | - Normalized regexes + auto-updating database of wappalyzer fingerprints. 11 | - Optimized for performance: parsing HTML manually for best speed. 12 | 13 | ### Using *go install* 14 | 15 | ```sh 16 | go install -v github.com/projectdiscovery/wappalyzergo/cmd/update-fingerprints@latest 17 | ``` 18 | 19 | After this command *wappalyzergo* library source will be in your current go.mod. 20 | 21 | ## Example 22 | Usage Example: 23 | 24 | ``` go 25 | package main 26 | 27 | import ( 28 | "fmt" 29 | "io" 30 | "log" 31 | "net/http" 32 | 33 | wappalyzer "github.com/projectdiscovery/wappalyzergo" 34 | ) 35 | 36 | func main() { 37 | resp, err := http.DefaultClient.Get("https://www.hackerone.com") 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | data, _ := io.ReadAll(resp.Body) // Ignoring error for example 42 | 43 | wappalyzerClient, err := wappalyzer.New() 44 | fingerprints := wappalyzerClient.Fingerprint(resp.Header, data) 45 | fmt.Printf("%v\n", fingerprints) 46 | 47 | // Output: map[Acquia Cloud Platform:{} Amazon EC2:{} Apache:{} Cloudflare:{} Drupal:{} PHP:{} Percona:{} React:{} Varnish:{}] 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /wappalyzer/fingerprint_headers.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | "github.com/chainreactors/fingers/common" 5 | "strings" 6 | ) 7 | 8 | // checkHeaders checks if the headers for a target match the fingerprints 9 | // and returns the matched IDs if any. 10 | func (engine *Wappalyze) checkHeaders(headers map[string]string) common.Frameworks { 11 | technologies := engine.fingerprints.matchMapString(headers, headersPart) 12 | return technologies 13 | } 14 | 15 | // normalizeHeaders normalizes the headers for the tech discovery on headers 16 | func (engine *Wappalyze) normalizeHeaders(headers map[string][]string) map[string]string { 17 | normalized := make(map[string]string, len(headers)) 18 | data := getHeadersMap(headers) 19 | 20 | for header, value := range data { 21 | normalized[strings.ToLower(header)] = strings.ToLower(value) 22 | } 23 | return normalized 24 | } 25 | 26 | // GetHeadersMap returns a map[string]string of response headers 27 | func getHeadersMap(headersArray map[string][]string) map[string]string { 28 | headers := make(map[string]string, len(headersArray)) 29 | 30 | builder := &strings.Builder{} 31 | for key, value := range headersArray { 32 | for i, v := range value { 33 | builder.WriteString(v) 34 | if i != len(value)-1 { 35 | builder.WriteString(", ") 36 | } 37 | } 38 | headerValue := builder.String() 39 | 40 | headers[key] = headerValue 41 | builder.Reset() 42 | } 43 | return headers 44 | } 45 | -------------------------------------------------------------------------------- /resources/loader.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "strings" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | // LoadResource loads content from file path or HTTP/HTTPS URL 14 | func LoadResource(path string) ([]byte, error) { 15 | // Check if it's a local file 16 | if _, err := os.Stat(path); err == nil { 17 | return os.ReadFile(path) 18 | } 19 | 20 | // Check if it's a URL 21 | if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { 22 | resp, err := http.Get(path) 23 | if err != nil { 24 | return nil, fmt.Errorf("failed to fetch from URL: %w", err) 25 | } 26 | defer resp.Body.Close() 27 | 28 | if resp.StatusCode != http.StatusOK { 29 | return nil, fmt.Errorf("bad status: %s", resp.Status) 30 | } 31 | 32 | return io.ReadAll(resp.Body) 33 | } 34 | 35 | return nil, fmt.Errorf("invalid resource path: %s (not a file or URL)", path) 36 | } 37 | 38 | // LoadFingersFromYAML loads fingerprints from YAML format file or URL 39 | // This function is specifically for loading custom fingerprints in YAML format 40 | func LoadFingersFromYAML(path string) ([]byte, error) { 41 | content, err := LoadResource(path) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | // Validate that it's valid YAML by attempting to unmarshal 47 | var test interface{} 48 | if err := yaml.Unmarshal(content, &test); err != nil { 49 | return nil, fmt.Errorf("invalid YAML format: %w", err) 50 | } 51 | 52 | return content, nil 53 | } 54 | -------------------------------------------------------------------------------- /fingers/matcher.go: -------------------------------------------------------------------------------- 1 | package fingers 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | func compileRegexp(s string) (*regexp.Regexp, error) { 9 | reg, err := regexp.Compile(s) 10 | if err != nil { 11 | return nil, err 12 | } 13 | return reg, nil 14 | } 15 | 16 | func compiledMatch(reg *regexp.Regexp, s []byte) (string, bool) { 17 | matched := reg.FindSubmatch(s) 18 | if matched == nil { 19 | return "", false 20 | } 21 | if len(matched) == 1 { 22 | return "", true 23 | } else { 24 | return strings.TrimSpace(string(matched[1])), true 25 | } 26 | } 27 | 28 | func compiledAllMatch(reg *regexp.Regexp, s string) ([]string, bool) { 29 | matchedes := reg.FindAllString(s, -1) 30 | if matchedes == nil { 31 | return nil, false 32 | } 33 | return matchedes, true 34 | } 35 | 36 | func RuleMatcher(rule *Rule, content *Content, ishttp bool) (bool, bool, string) { 37 | var hasFrame, hasVuln bool 38 | var version string 39 | if rule.Regexps == nil { 40 | return false, false, "" 41 | } 42 | 43 | hasFrame, hasVuln, version = rule.Match(content.Content, content.Header, content.Body) 44 | if hasFrame || !ishttp { 45 | return hasFrame, hasVuln, version 46 | } 47 | 48 | if content.Cert != "" { 49 | hasFrame = rule.MatchCert(content.Cert) 50 | } 51 | 52 | if version == "" && rule.Regexps.CompiledVersionRegexp != nil { 53 | for _, reg := range rule.Regexps.CompiledVersionRegexp { 54 | version, _ = compiledMatch(reg, content.Content) 55 | } 56 | } 57 | return hasFrame, hasVuln, version 58 | } 59 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # overview 2 | 3 | repo: https://github.com/chainreactors/fingers 4 | 5 | fingers 是用来各种指纹规则库的go实现, 不同规则库的语法不同, 为了支持在工具多规则库. 于是新增了fingers仓库管理各种不同的规则引擎, 允许不同的输入结构, 但统一输出结构. 并合并输出结果, 最大化指纹识别能力 6 | 7 | 目前fingers仓库已经成为[spray](https://github.com/chainreactors/spray) 与 [gogo](https://github.com/chainreactors/gogo)的指纹引擎. 后续将移植到更多工具中, 也欢迎其他工具使用本仓库. 8 | 9 | ## Features 10 | 11 | ### 指纹库 12 | 13 | fingers engine 通过实现多个指纹库的解析, 实现一次扫描多个指纹库匹配。最大程度提升指纹能力 14 | 15 | #### fingers 16 | 17 | fingers原生支持的指纹库, 也是目前支持最多特性的指纹库 18 | 19 | !!! example "Features." 20 | * 支持多种方式规则配置 21 | * 支持多种方式的版本号匹配 22 | * 404/favicon/waf/cdn/供应链指纹识别 23 | * 主动指纹识别 24 | * 超强性能, 采用了缓存,正则预编译,默认端口,优先级等等算法提高引擎性能 25 | * 重点指纹,指纹来源与tag标记 26 | 27 | 具体语法请见 #DSL 28 | 29 | #### wappalyzer 30 | 31 | https://github.com/chainreactors/fingers/tree/master/wappalyzer 为wappalyzer指纹库的实现, 核心代码fork自 https://github.com/projectdiscovery/wappalyzergo , 将其输出结果统一为frameworks. 32 | 33 | 后续将会提供每周更新的github action, 规则库只做同步. 34 | 35 | #### fingerprinthub 36 | 37 | 规则库本体位于: https://github.com/0x727/FingerprintHub 38 | 39 | https://github.com/chainreactors/fingers/tree/master/fingerprinthub 为其规则库的go实现. 本仓库的此规则库只做同步. 40 | 41 | 后续将会提供每周更新的github action, 规则库只做同步. 42 | 43 | #### ehole 44 | 45 | 规则库本体位于: https://github.com/EdgeSecurityTeam/EHole 46 | 47 | https://github.com/chainreactors/fingers/tree/master/ehole 为其规则库的go实现. 本仓库的此规则库只做同步. 48 | 49 | #### goby 50 | 51 | 规则库本体来自开源社区的逆向[goby](https://gobies.org/) Thanks @XiaoliChan @9bie . 52 | 53 | https://github.com/chainreactors/fingers/tree/master/goby 为其规则库的go实现. 本仓库的此规则库只做同步. 54 | -------------------------------------------------------------------------------- /nmap/type-portlist.go: -------------------------------------------------------------------------------- 1 | package gonmap 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | var portRangeRegx = regexp.MustCompile("^(\\d+)(?:-(\\d+))?$") 10 | var portGroupRegx = regexp.MustCompile("^(\\d+(?:-\\d+)?)(?:,\\d+(?:-\\d+)?)*$") 11 | 12 | type PortList []int 13 | 14 | var emptyPortList = PortList([]int{}) 15 | 16 | func parsePortList(express string) PortList { 17 | var list = PortList([]int{}) 18 | if portGroupRegx.MatchString(express) == false { 19 | panic("port expression string invalid") 20 | } 21 | for _, expr := range strings.Split(express, ",") { 22 | rArr := portRangeRegx.FindStringSubmatch(expr) 23 | var startPort, endPort int 24 | startPort, _ = strconv.Atoi(rArr[1]) 25 | if rArr[2] != "" { 26 | endPort, _ = strconv.Atoi(rArr[2]) 27 | } else { 28 | endPort = startPort 29 | } 30 | for num := startPort; num <= endPort; num++ { 31 | list = append(list, num) 32 | } 33 | } 34 | list = list.removeDuplicate() 35 | return list 36 | } 37 | 38 | func (p PortList) removeDuplicate() PortList { 39 | result := make([]int, 0, len(p)) 40 | temp := map[int]struct{}{} 41 | for _, item := range p { 42 | if _, ok := temp[item]; !ok { //如果字典中找不到元素,ok=false,!ok为true,就往切片中append元素。 43 | temp[item] = struct{}{} 44 | result = append(result, item) 45 | } 46 | } 47 | return result 48 | } 49 | 50 | func (p PortList) exist(port int) bool { 51 | for _, num := range p { 52 | if num == port { 53 | return true 54 | } 55 | } 56 | return false 57 | } 58 | 59 | func (p PortList) append(ports ...int) PortList { 60 | p = append(p, ports...) 61 | p = p.removeDuplicate() 62 | return p 63 | } 64 | -------------------------------------------------------------------------------- /wappalyzer/fingerprint_cookies.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | "github.com/chainreactors/fingers/common" 5 | "strings" 6 | ) 7 | 8 | // checkCookies checks if the cookies for a target match the fingerprints 9 | // and returns the matched IDs if any. 10 | func (engine *Wappalyze) checkCookies(cookies []string) common.Frameworks { 11 | // Normalize the cookies for further processing 12 | normalized := engine.normalizeCookies(cookies) 13 | 14 | technologies := engine.fingerprints.matchMapString(normalized, cookiesPart) 15 | return technologies 16 | } 17 | 18 | const keyValuePairLength = 2 19 | 20 | // normalizeCookies normalizes the cookies and returns an 21 | // easily parsed format that can be processed upon. 22 | func (engine *Wappalyze) normalizeCookies(cookies []string) map[string]string { 23 | normalized := make(map[string]string) 24 | 25 | for _, part := range cookies { 26 | parts := strings.SplitN(strings.Trim(part, " "), "=", keyValuePairLength) 27 | if len(parts) < keyValuePairLength { 28 | continue 29 | } 30 | normalized[parts[0]] = parts[1] 31 | } 32 | return normalized 33 | } 34 | 35 | // findSetCookie finds the set cookie header from the normalized headers 36 | func (engine *Wappalyze) findSetCookie(headers map[string]string) []string { 37 | value, ok := headers["set-cookie"] 38 | if !ok { 39 | return nil 40 | } 41 | 42 | var values []string 43 | for _, v := range strings.Split(value, " ") { 44 | if v == "" { 45 | continue 46 | } 47 | if strings.Contains(v, ",") { 48 | values = append(values, strings.Split(v, ",")...) 49 | } else if strings.Contains(v, ";") { 50 | values = append(values, strings.Split(v, ";")...) 51 | } else { 52 | values = append(values, v) 53 | } 54 | } 55 | return values 56 | } 57 | -------------------------------------------------------------------------------- /favicon/favicon.go: -------------------------------------------------------------------------------- 1 | package favicon 2 | 3 | import ( 4 | "github.com/chainreactors/fingers/common" 5 | "github.com/chainreactors/utils/encode" 6 | ) 7 | 8 | func NewFavicons() *FaviconsEngine { 9 | return &FaviconsEngine{ 10 | Md5Fingers: make(map[string]string), 11 | Mmh3Fingers: make(map[string]string), 12 | } 13 | } 14 | 15 | type FaviconsEngine struct { 16 | Md5Fingers map[string]string 17 | Mmh3Fingers map[string]string 18 | } 19 | 20 | func (engine *FaviconsEngine) Compile() error { 21 | return nil 22 | } 23 | 24 | func (engine *FaviconsEngine) Name() string { 25 | return "favicon" 26 | } 27 | 28 | func (engine *FaviconsEngine) Len() int { 29 | return len(engine.Md5Fingers) + len(engine.Mmh3Fingers) 30 | } 31 | 32 | func (engine *FaviconsEngine) HashMatch(md5, mmh3 string) *common.Framework { 33 | if engine.Md5Fingers[md5] != "" { 34 | return common.NewFramework(engine.Md5Fingers[md5], common.FrameFromICO) 35 | } 36 | 37 | if engine.Mmh3Fingers[mmh3] != "" { 38 | return common.NewFramework(engine.Mmh3Fingers[mmh3], common.FrameFromICO) 39 | } 40 | return nil 41 | } 42 | 43 | // WebMatch 实现Web指纹匹配 44 | func (engine *FaviconsEngine) WebMatch(content []byte) common.Frameworks { 45 | md5h := encode.Md5Hash(content) 46 | mmh3h := encode.Mmh3Hash32(content) 47 | fs := make(common.Frameworks) 48 | fs.Add(engine.HashMatch(md5h, mmh3h)) 49 | return fs 50 | } 51 | 52 | // ServiceMatch 实现Service指纹匹配 - favicon不支持Service指纹 53 | func (engine *FaviconsEngine) ServiceMatch(host string, portStr string, level int, sender common.ServiceSender, callback common.ServiceCallback) *common.ServiceResult { 54 | // favicon不支持Service指纹识别 55 | return nil 56 | } 57 | 58 | func (engine *FaviconsEngine) Capability() common.EngineCapability { 59 | return common.EngineCapability{ 60 | SupportWeb: true, // favicon支持Web指纹(特定的favicon指纹) 61 | SupportService: false, // favicon不支持Service指纹 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /resources/embed.go: -------------------------------------------------------------------------------- 1 | //go:build !noembed && go1.16 2 | // +build !noembed,go1.16 3 | 4 | package resources 5 | 6 | import ( 7 | _ "embed" 8 | "github.com/chainreactors/utils" 9 | "github.com/chainreactors/utils/encode" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | //go:embed aliases.yaml 14 | var AliasesData []byte 15 | 16 | //go:embed port.yaml 17 | var PortData []byte // yaml format 18 | 19 | var PrePort *utils.PortPreset 20 | 21 | func LoadPorts() (*utils.PortPreset, error) { 22 | var ports []*utils.PortConfig 23 | var err error 24 | err = yaml.Unmarshal(PortData, &ports) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | PrePort = utils.NewPortPreset(ports) 30 | return PrePort, nil 31 | } 32 | 33 | // engine 34 | 35 | var ( 36 | //go:embed goby.json.gz 37 | GobyData []byte 38 | 39 | //go:embed fingerprinthub_web.json.gz 40 | FingerprinthubWebData []byte 41 | 42 | //go:embed fingerprinthub_service.json.gz 43 | FingerprinthubServiceData []byte 44 | 45 | //go:embed ehole.json.gz 46 | EholeData []byte 47 | 48 | //go:embed fingers_http.json.gz 49 | FingersHTTPData []byte 50 | 51 | //go:embed fingers_socket.json.gz 52 | FingersSocketData []byte 53 | 54 | //go:embed wappalyzer.json.gz 55 | WappalyzerData []byte 56 | 57 | //go:embed nmap-service-probes.json.gz 58 | NmapServiceProbesData []byte 59 | 60 | //go:embed nmap-services.json.gz 61 | NmapServicesData []byte 62 | 63 | CheckSum = map[string]string{ 64 | "goby": encode.Md5Hash(GobyData), 65 | "fingerprinthub_web": encode.Md5Hash(FingerprinthubWebData), 66 | "fingerprinthub_service": encode.Md5Hash(FingerprinthubServiceData), 67 | "ehole": encode.Md5Hash(EholeData), 68 | "fingers": encode.Md5Hash(FingersHTTPData), 69 | "fingers_socket": encode.Md5Hash(FingersSocketData), 70 | "wappalyzer": encode.Md5Hash(WappalyzerData), 71 | "nmap": encode.Md5Hash(NmapServiceProbesData), 72 | "nmap_services": encode.Md5Hash(NmapServicesData), 73 | "alias": encode.Md5Hash(AliasesData), 74 | "port": encode.Md5Hash(PortData), 75 | } 76 | ) 77 | -------------------------------------------------------------------------------- /cmd/README.md: -------------------------------------------------------------------------------- 1 | # CMD 工具集 2 | 3 | Fingers 库提供了一系列命令行工具,用于指纹验证、测试和转换等操作。 4 | 5 | ## 可用工具 6 | 7 | ### validate - 指纹验证工具 8 | 9 | **位置**: `cmd/validate/` 10 | **功能**: 验证 fingers 和 alias 格式文件的语法正确性 11 | 12 | **主要特性**: 13 | - 支持 fingers 指纹文件验证 14 | - 支持 alias 别名文件验证 15 | - 生成 JSON Schema 16 | - 详细的错误报告和统计 17 | 18 | **快速使用**: 19 | ```bash 20 | cd cmd/validate 21 | # 验证指纹文件 22 | go run main.go -engine fingers fingerprints.yaml 23 | # 验证别名文件 24 | go run main.go -engine alias aliases.yaml 25 | # 查看帮助 26 | go run main.go -help 27 | ``` 28 | 29 | ### test - 指纹测试工具 30 | 31 | **位置**: `cmd/test/` 32 | **功能**: 对实际目标进行指纹检测和验证 33 | 34 | **主要特性**: 35 | - 通用指纹检测 36 | - Alias 配置测试 37 | - 目标覆盖功能 38 | - 指纹匹配验证 39 | - 详细的测试报告 40 | 41 | **快速使用**: 42 | ```bash 43 | cd cmd/test 44 | # 通用指纹检测 45 | go run main.go -target https://nginx.org -detect-all 46 | # Alias测试 47 | go run main.go -alias aliases.yaml -name nginx_test 48 | # 查看帮助 49 | go run main.go -help 50 | ``` 51 | 52 | ### transform - 数据转换工具 53 | 54 | **位置**: `cmd/transform/` 55 | **功能**: 转换不同格式的指纹数据 56 | 57 | **主要特性**: 58 | - 支持多种指纹格式转换 59 | - 批量处理能力 60 | - 数据清洗和标准化 61 | 62 | **快速使用**: 63 | ```bash 64 | cd cmd/transform 65 | go run transform.go [options] 66 | ``` 67 | 68 | ### nmap - Nmap 服务探测 69 | 70 | **位置**: `cmd/nmap/` 71 | **功能**: 基于 Nmap service-probes 的服务指纹识别 72 | 73 | **主要特性**: 74 | - TCP/UDP 服务探测 75 | - 基于 nmap-service-probes 数据库 76 | - 端口服务识别 77 | 78 | **快速使用**: 79 | ```bash 80 | cd cmd/nmap 81 | go run nmap.go [target] [port] 82 | ``` 83 | 84 | ### engine - 引擎示例 85 | 86 | **位置**: `cmd/engine/` 87 | **功能**: 展示如何使用 Fingers 引擎的示例代码 88 | 89 | **主要特性**: 90 | - 引擎初始化示例 91 | - Web 指纹检测示例 92 | - Service 指纹检测示例 93 | 94 | **快速使用**: 95 | ```bash 96 | cd cmd/engine 97 | go run example.go 98 | ``` 99 | 100 | ## 工具链使用流程 101 | 102 | ### 1. 开发阶段 103 | 使用 `validate` 工具验证指纹文件格式: 104 | ```bash 105 | cd cmd/validate 106 | go run main.go -engine fingers new_fingerprints.yaml 107 | ``` 108 | 109 | ### 2. 测试阶段 110 | 使用 `test` 工具测试指纹准确性: 111 | ```bash 112 | cd cmd/test 113 | go run main.go -alias test_aliases.yaml -name new_fingerprint_test 114 | ``` 115 | 116 | ### 3. 部署阶段 117 | 集成到应用程序中使用 Fingers 引擎进行实际检测。 118 | 119 | ## 注意事项 120 | 121 | - 所有工具都支持 `-help` 参数查看详细使用说明 122 | - 工具需要在对应目录下运行,或使用完整路径 123 | - 部分工具需要网络连接进行实际测试 124 | - 建议在测试环境中先验证功能后再用于生产环境 125 | 126 | ## 贡献 127 | 128 | 如需添加新的命令行工具,请: 129 | 1. 在 `cmd/` 下创建新目录 130 | 2. 实现相应功能 131 | 3. 添加使用文档 132 | 4. 更新本 README 文件 -------------------------------------------------------------------------------- /wappalyzer/wappalyzergo_test.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/chainreactors/fingers/resources" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCookiesDetect(t *testing.T) { 11 | wappalyzer, err := NewWappalyzeEngine(resources.WappalyzerData) 12 | require.Nil(t, err, "could not create wappalyzer") 13 | 14 | matches := wappalyzer.Fingerprint(map[string][]string{ 15 | "Set-Cookie": {"_uetsid=ABCDEF"}, 16 | }, []byte("")) 17 | 18 | require.Contains(t, matches, "Microsoft Advertising", "Could not get correct match") 19 | 20 | t.Run("position", func(t *testing.T) { 21 | wappalyzerClient, _ := NewWappalyzeEngine(resources.WappalyzerData) 22 | 23 | fingerprints := wappalyzerClient.Fingerprint(map[string][]string{ 24 | "Set-Cookie": {"path=/; jsessionid=111; path=/, jsessionid=111;"}, 25 | }, []byte("")) 26 | fingerprints1 := wappalyzerClient.Fingerprint(map[string][]string{ 27 | "Set-Cookie": {"jsessionid=111; path=/, XSRF-TOKEN=; expires=test, path=/ laravel_session=eyJ*"}, 28 | }, []byte("")) 29 | 30 | require.Equal(t, map[string]struct{}{"Java": {}}, fingerprints, "could not get correct fingerprints") 31 | require.Equal(t, map[string]struct{}{"Java": {}, "Laravel": {}, "PHP": {}}, fingerprints1, "could not get correct fingerprints") 32 | }) 33 | } 34 | 35 | func TestHeadersDetect(t *testing.T) { 36 | wappalyzer, err := NewWappalyzeEngine(resources.WappalyzerData) 37 | require.Nil(t, err, "could not create wappalyzer") 38 | 39 | matches := wappalyzer.Fingerprint(map[string][]string{ 40 | "Server": {"now"}, 41 | }, []byte("")) 42 | 43 | require.Contains(t, matches, "Vercel", "Could not get correct match") 44 | } 45 | 46 | func TestBodyDetect(t *testing.T) { 47 | wappalyzer, err := NewWappalyzeEngine(resources.WappalyzerData) 48 | require.Nil(t, err, "could not create wappalyzer") 49 | 50 | t.Run("meta", func(t *testing.T) { 51 | matches := wappalyzer.Fingerprint(map[string][]string{}, []byte(` 52 | 53 | 54 | 55 | `)) 56 | require.Contains(t, matches, "Mura CMS:1", "Could not get correct match") 57 | }) 58 | 59 | t.Run("html-implied", func(t *testing.T) { 60 | matches := wappalyzer.Fingerprint(map[string][]string{}, []byte(` 61 | 62 | 63 | 64 | 65 | `)) 66 | require.Contains(t, matches, "AngularJS", "Could not get correct implied match") 67 | require.Contains(t, matches, "PHP", "Could not get correct implied match") 68 | require.Contains(t, matches, "Proximis Unified Commerce", "Could not get correct match") 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /cmd/engine/README.md: -------------------------------------------------------------------------------- 1 | # Fingers Example 2 | 3 | 这是一个使用 Fingers 引擎进行指纹识别的命令行工具示例。 4 | 5 | ## 功能特性 6 | 7 | - 支持多引擎指纹识别 8 | - 支持 SSL 证书验证跳过 9 | - 支持详细输出模式 10 | - 支持 Favicon 专项检测 11 | - 支持自定义资源文件覆盖 12 | 13 | ## 使用方法 14 | 15 | ### 基本用法 16 | 17 | ```bash 18 | # 基本使用 19 | go run example.go -u https://example.com 20 | 21 | # 使用特定引擎 22 | go run example.go -u https://example.com -e fingers,wappalyzer 23 | 24 | # 显示详细信息 25 | go run example.go -u https://example.com -v 26 | 27 | # 忽略SSL证书验证 28 | go run example.go -u https://example.com -k 29 | 30 | # 仅检测favicon 31 | go run example.go -u https://example.com -f 32 | ``` 33 | 34 | ### 资源文件覆盖 35 | 36 | 工具支持覆盖所有内置的指纹库文件: 37 | 38 | ```bash 39 | # Goby 指纹库 40 | go run example.go -u https://example.com --goby custom_goby.json 41 | 42 | # FingerPrintHub 指纹库 43 | go run example.go -u https://example.com --fingerprinthub custom_fingerprinthub.json 44 | 45 | # EHole 指纹库 46 | go run example.go -u https://example.com --ehole custom_ehole.json 47 | 48 | # Fingers 指纹库 49 | go run example.go -u https://example.com --fingers custom_fingers.json 50 | 51 | # Wappalyzer 指纹库 52 | go run example.go -u https://example.com --wappalyzer custom_wappalyzer.json 53 | 54 | # Aliases 配置 55 | go run example.go -u https://example.com --aliases custom_aliases.yaml 56 | ``` 57 | 58 | 资源文件说明: 59 | 60 | - 支持 JSON 和 YAML 格式的资源文件 61 | - JSON 文件会自动转换为 YAML 格式 62 | - 非压缩文件会自动进行 gzip 压缩 63 | - 可以同时覆盖多个资源文件 64 | 65 | ## 命令行参数 66 | 67 | ``` 68 | 应用选项: 69 | -e, --engines= 指定要使用的引擎,多个引擎用逗号分隔 (默认: fingers,fingerprinthub,wappalyzer,ehole,goby) 70 | -k, --insecure 跳过 SSL 证书验证 71 | -u, --url= 要检测的目标 URL (必需) 72 | -v, --verbose 显示详细调试信息 73 | -f, --favicon 仅检测 favicon 74 | --goby= 覆盖 goby.json.gz 75 | --fingerprinthub= 覆盖 fingerprinthub_v3.json.gz 76 | --ehole= 覆盖 ehole.json.gz 77 | --fingers= 覆盖 fingers_http.json.gz 78 | --wappalyzer= 覆盖 wappalyzer.json.gz 79 | --aliases= 覆盖 aliases.yaml 80 | ``` 81 | 82 | ## 输出示例 83 | 84 | 普通模式: 85 | 86 | ``` 87 | nginx/1.18.0 ubuntu/20.04 88 | ``` 89 | 90 | 详细模式 (-v): 91 | 92 | ``` 93 | Loaded engines: fingers:1000 fingerprinthub:500 wappalyzer:300 94 | 95 | Detected frameworks for https://example.com: 96 | Name: nginx 97 | Vendor: nginx 98 | Product: nginx 99 | Version: 1.18.0 100 | CPE: cpe:/a:nginx:nginx:1.18.0 101 | --- 102 | Name: ubuntu 103 | Vendor: canonical 104 | Product: ubuntu 105 | Version: 20.04 106 | CPE: cpe:/o:canonical:ubuntu:20.04 107 | --- 108 | ``` 109 | 110 | ## 注意事项 111 | 112 | 1. 自定义资源文件必须符合对应引擎的数据格式要求 113 | 2. 建议先使用小规模数据测试自定义资源文件是否正确 114 | 3. 覆盖资源文件会影响所有使用该资源的引擎 115 | 4. 建议在测试环境中充分验证自定义资源文件后再在生产环境使用 116 | -------------------------------------------------------------------------------- /common/attributes.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/facebookincubator/nvdtools/wfn" 4 | 5 | func NewAttributesWithAny() *Attributes { 6 | return &Attributes{} 7 | } 8 | 9 | func NewAttributesWithCPE(s string) *Attributes { 10 | attr, err := wfn.Parse(s) 11 | if err != nil { 12 | return nil 13 | } 14 | return &Attributes{ 15 | Part: attr.Part, 16 | Vendor: attr.Vendor, 17 | Product: attr.Product, 18 | Version: attr.Version, 19 | Update: attr.Update, 20 | Edition: attr.Edition, 21 | SWEdition: attr.SWEdition, 22 | TargetSW: attr.TargetSW, 23 | TargetHW: attr.TargetHW, 24 | Other: attr.Other, 25 | Language: attr.Language, 26 | } 27 | } 28 | 29 | type Attributes struct { 30 | Part string `json:"part,omitempty" yaml:"part" jsonschema:"title=Part,description=Component part identifier"` 31 | Vendor string `json:"vendor,omitempty" yaml:"vendor" jsonschema:"title=Vendor,description=Vendor or manufacturer name"` 32 | Product string `json:"product,omitempty" yaml:"product" jsonschema:"title=Product,description=Product name"` 33 | Version string `json:"version,omitempty" yaml:"version,omitempty" jsonschema:"title=Version,description=Product version"` 34 | Update string `json:"update,omitempty" yaml:"update,omitempty" jsonschema:"title=Update,description=Product update version"` 35 | Edition string `json:"edition,omitempty" yaml:"edition,omitempty" jsonschema:"title=Edition,description=Product edition"` 36 | SWEdition string `json:"sw_edition,omitempty" yaml:"sw_edition,omitempty" jsonschema:"title=Software Edition,description=Software edition"` 37 | TargetSW string `json:"target_sw,omitempty" yaml:"target_sw,omitempty" jsonschema:"title=Target Software,description=Target software"` 38 | TargetHW string `json:"target_hw,omitempty" yaml:"target_hw,omitempty" jsonschema:"title=Target Hardware,description=Target hardware"` 39 | Other string `json:"other,omitempty" yaml:"other,omitempty" jsonschema:"title=Other,description=Other attributes"` 40 | Language string `json:"language,omitempty" yaml:"language,omitempty" jsonschema:"title=Language,description=Language identifier"` 41 | } 42 | 43 | func (a *Attributes) WFN() *wfn.Attributes { 44 | return &wfn.Attributes{ 45 | Part: a.Part, 46 | Vendor: a.Vendor, 47 | Product: a.Product, 48 | Version: a.Version, 49 | Update: a.Update, 50 | Edition: a.Edition, 51 | SWEdition: a.SWEdition, 52 | TargetSW: a.TargetSW, 53 | TargetHW: a.TargetHW, 54 | Other: a.Other, 55 | } 56 | } 57 | 58 | func (a *Attributes) URI() string { 59 | return a.WFN().BindToURI() 60 | } 61 | 62 | func (a *Attributes) String() string { 63 | return a.WFN().BindToFmtString() 64 | } 65 | 66 | func (a *Attributes) WFNString() string { 67 | return a.WFN().String() 68 | } 69 | -------------------------------------------------------------------------------- /nmap/init_data.go: -------------------------------------------------------------------------------- 1 | package gonmap 2 | 3 | import ( 4 | "compress/gzip" 5 | "encoding/json" 6 | "strings" 7 | ) 8 | 9 | // loadServicesFromBytes 从bytes加载services数据 10 | func (n *Nmap) loadServicesFromBytes(servicesData []byte) { 11 | // 从bytes加载nmap-services.json.gz 12 | reader, err := gzip.NewReader(strings.NewReader(string(servicesData))) 13 | if err != nil { 14 | return // 忽略错误,使用默认值 15 | } 16 | defer reader.Close() 17 | 18 | // 解析JSON数据 19 | decoder := json.NewDecoder(reader) 20 | var data ServicesData 21 | err = decoder.Decode(&data) 22 | if err != nil { 23 | return // 忽略错误,使用默认值 24 | } 25 | 26 | // 保存数据 27 | n.servicesData = &data 28 | 29 | // 构建nmapServices数组以保持兼容性 30 | n.nmapServices = n.buildNmapServicesArray(&data) 31 | } 32 | 33 | // loadProbesFromBytes 从bytes加载probes数据 34 | func (n *Nmap) loadProbesFromBytes(probesData []byte) { 35 | // 从bytes加载压缩的nmap数据 36 | reader, err := gzip.NewReader(strings.NewReader(string(probesData))) 37 | if err != nil { 38 | return 39 | } 40 | defer reader.Close() 41 | 42 | // 创建JSON解码器 43 | decoder := json.NewDecoder(reader) 44 | var data NmapProbesData 45 | 46 | // 解码JSON数据 47 | err = decoder.Decode(&data) 48 | if err != nil { 49 | return 50 | } 51 | 52 | // 加载探针数据并重新编译正则表达式 53 | for _, probe := range data.Probes { 54 | // 重新编译每个Match中的正则表达式 55 | for _, match := range probe.MatchGroup { 56 | // 重新编译PatternRegexp,从JSON反序列化时不会保存正则对象 57 | match.PatternRegexp = match.getPatternRegexp(match.Pattern, "") 58 | } 59 | n.pushProbe(*probe) 60 | } 61 | } 62 | 63 | // addCustomMatches 添加自定义指纹 64 | func (n *Nmap) addCustomMatches() { 65 | //新增自定义指纹信息 66 | n.AddMatch("TCP_GetRequest", `echo m|^GET / HTTP/1.0\r\n\r\n$|s`) 67 | n.AddMatch("TCP_GetRequest", `mongodb m|.*It looks like you are trying to access MongoDB.*|s p/MongoDB/`) 68 | n.AddMatch("TCP_GetRequest", `http m|^HTTP/1\.[01] \d\d\d (?:[^\r\n]+\r\n)*?Server: ([^\r\n]+)| p/$1/`) 69 | n.AddMatch("TCP_GetRequest", `http m|^HTTP/1\.[01] \d\d\d|`) 70 | n.AddMatch("TCP_NULL", `mysql m|.\x00\x00..j\x04Host '.*' is not allowed to connect to this MariaDB server| p/MariaDB/`) 71 | n.AddMatch("TCP_NULL", `mysql m|.\x00\x00\x00\x0a([\d.]+)\x00.*MariaDB| p/MariaDB/ v/$1/`) 72 | n.AddMatch("TCP_NULL", `mysql m|.\x00\x00\x00\x0a([\d.]+)\x00| p/MySQL/ v/$1/`) 73 | } 74 | 75 | // optimizeProbes 优化探针配置 76 | func (n *Nmap) optimizeProbes() { 77 | // HTTP端口优化 78 | httpPorts := []int{80, 443, 8080, 8443, 8000, 8888, 9090} 79 | for _, port := range httpPorts { 80 | if port < len(n.portProbeMap) { 81 | // 将HTTP探针放在前面 82 | n.portProbeMap[port] = append([]string{"TCP_GetRequest"}, n.portProbeMap[port]...) 83 | } 84 | } 85 | 86 | // SSL端口优化 87 | sslPorts := []int{443, 8443, 3389} 88 | for _, port := range sslPorts { 89 | if port < len(n.portProbeMap) { 90 | // 将SSL探针放在前面 91 | for _, sslProbe := range n.sslProbeMap { 92 | n.portProbeMap[port] = append([]string{sslProbe}, n.portProbeMap[port]...) 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /nmap/nmap-services.go: -------------------------------------------------------------------------------- 1 | package gonmap 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Service 代表一个服务条目 9 | type Service struct { 10 | Name string `json:"name"` 11 | Port int `json:"port"` 12 | Protocol string `json:"protocol"` 13 | Probability float64 `json:"probability"` 14 | Comments string `json:"comments,omitempty"` 15 | } 16 | 17 | // ServicesData 代表解析后的services数据结构 18 | type ServicesData struct { 19 | Services []Service `json:"services"` 20 | } 21 | 22 | // loadServicesData 加载services数据,为了兼容性保留,但使用全局nmap实例 23 | func loadServicesData() (*ServicesData, error) { 24 | // 确保nmap已经初始化 25 | if nmap == nil { 26 | initNmap() 27 | } 28 | 29 | // 返回nmap实例中的ServicesData 30 | if nmap.servicesData != nil { 31 | return nmap.servicesData, nil 32 | } 33 | 34 | return nil, fmt.Errorf("services data not loaded") 35 | } 36 | 37 | // GetPortsByServiceName 通过服务名称快速获取端口列表 38 | func GetPortsByServiceName(serviceName string) ([]int, error) { 39 | data, err := loadServicesData() 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | serviceName = fixServiceName(serviceName) 45 | var ports []int 46 | 47 | for _, service := range data.Services { 48 | if fixServiceName(service.Name) == serviceName { 49 | ports = append(ports, service.Port) 50 | } 51 | } 52 | 53 | if len(ports) == 0 { 54 | return nil, fmt.Errorf("service %s not found", serviceName) 55 | } 56 | 57 | return ports, nil 58 | } 59 | 60 | // GetNmapServices 返回services数组,保持向后兼容 61 | func GetNmapServices() ([]string, error) { 62 | // 确保nmap已经初始化 63 | if nmap == nil { 64 | initNmap() 65 | } 66 | 67 | if nmap.nmapServices != nil { 68 | return nmap.nmapServices, nil 69 | } 70 | return nil, fmt.Errorf("nmap services not loaded") 71 | } 72 | 73 | // GetServicesData 返回完整的services数据结构 74 | func GetServicesData() (*ServicesData, error) { 75 | return loadServicesData() 76 | } 77 | 78 | // GetServiceByPort 根据端口号获取服务名称 79 | func GetServiceByPort(port int) (string, error) { 80 | // 确保nmap已经初始化 81 | if nmap == nil { 82 | initNmap() 83 | } 84 | 85 | if port >= 0 && port < len(nmap.nmapServices) { 86 | return nmap.nmapServices[port], nil 87 | } 88 | return "unknown", nil 89 | } 90 | 91 | // SearchServicesByName 根据服务名称搜索相关服务 92 | func SearchServicesByName(name string) ([]Service, error) { 93 | data, err := loadServicesData() 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | var results []Service 99 | searchName := strings.ToLower(name) 100 | 101 | for _, service := range data.Services { 102 | if strings.Contains(strings.ToLower(service.Name), searchName) { 103 | results = append(results, service) 104 | } 105 | } 106 | 107 | return results, nil 108 | } 109 | 110 | // fixServiceName 修复服务名称(本地版本避免重复声明) 111 | func fixServiceName(serviceName string) string { 112 | // 确保nmap已经初始化 113 | if nmap == nil { 114 | initNmap() 115 | } 116 | return nmap.fixServiceName(serviceName) 117 | } -------------------------------------------------------------------------------- /goby/goby.go: -------------------------------------------------------------------------------- 1 | package goby 2 | 3 | import ( 4 | "bytes" 5 | "github.com/chainreactors/fingers/common" 6 | "github.com/chainreactors/fingers/resources" 7 | "github.com/chainreactors/words/logic" 8 | "strings" 9 | ) 10 | 11 | func NewGobyEngine(data []byte) (*GobyEngine, error) { 12 | var fingers []*GobyFinger 13 | err := resources.UnmarshalData(data, &fingers) 14 | if err != nil { 15 | return nil, err 16 | } 17 | engine := &GobyEngine{ 18 | Fingers: fingers, 19 | } 20 | err = engine.Compile() 21 | if err != nil { 22 | return nil, err 23 | } 24 | return engine, nil 25 | } 26 | 27 | type GobyEngine struct { 28 | Fingers []*GobyFinger 29 | } 30 | 31 | func (engine *GobyEngine) Name() string { 32 | return "goby" 33 | } 34 | 35 | func (engine *GobyEngine) Len() int { 36 | return len(engine.Fingers) 37 | } 38 | 39 | func (engine *GobyEngine) Compile() error { 40 | for _, finger := range engine.Fingers { 41 | err := finger.Compile() 42 | if err != nil { 43 | return err 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | // WebMatch 实现Web指纹匹配 50 | func (engine *GobyEngine) WebMatch(content []byte) common.Frameworks { 51 | return engine.MatchRaw(string(bytes.ToLower(content))) 52 | } 53 | 54 | // ServiceMatch 实现Service指纹匹配 - goby不支持Service指纹 55 | func (engine *GobyEngine) ServiceMatch(host string, portStr string, level int, sender common.ServiceSender, callback common.ServiceCallback) *common.ServiceResult { 56 | // goby不支持Service指纹识别 57 | return nil 58 | } 59 | 60 | func (engine *GobyEngine) Capability() common.EngineCapability { 61 | return common.EngineCapability{ 62 | SupportWeb: true, // goby支持Web指纹 63 | SupportService: false, // goby不支持Service指纹 64 | } 65 | } 66 | 67 | func (engine *GobyEngine) MatchRaw(raw string) common.Frameworks { 68 | frames := make(common.Frameworks) 69 | for _, finger := range engine.Fingers { 70 | frame := finger.Match(raw) 71 | if frame != nil { 72 | frames.Add(frame) 73 | } 74 | } 75 | return frames 76 | } 77 | 78 | type gobyRule struct { 79 | Label string `json:"label"` 80 | Feature string `json:"feature"` 81 | IsEquel bool `json:"is_equal"` //是则判断条件相等,否则判断不等 82 | } 83 | 84 | type GobyFinger struct { 85 | Logic string `json:"logic"` 86 | logicExpr *logic.Program 87 | Name string `json:"name"` 88 | Rule []gobyRule `json:"rule"` 89 | } 90 | 91 | func (finger *GobyFinger) Compile() error { 92 | for i, r := range finger.Rule { 93 | // Fix bug: golang 不支持直接使用 `r.Feature` 的方式修改循环内的值 94 | //r.Feature = strings.ToLower(r.Feature) 95 | finger.Rule[i].Feature = strings.ToLower(r.Feature) 96 | } 97 | 98 | finger.logicExpr = logic.Compile(finger.Logic) 99 | return nil 100 | } 101 | 102 | func (finger *GobyFinger) Match(raw string) *common.Framework { 103 | env := make(map[string]bool) 104 | for _, r := range finger.Rule { 105 | match := strings.Contains(raw, r.Feature) 106 | env[r.Label] = match == r.IsEquel 107 | } 108 | 109 | matched := logic.EvalLogic(finger.logicExpr, env) 110 | 111 | if matched { 112 | return common.NewFramework(finger.Name, common.FrameFromGoby) 113 | } 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /common/vuln.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "github.com/chainreactors/utils/iutils" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | SeverityINFO int = iota + 1 11 | SeverityMEDIUM 12 | SeverityHIGH 13 | SeverityCRITICAL 14 | SeverityUnknown 15 | ) 16 | 17 | func GetSeverityLevel(s string) int { 18 | switch s { 19 | case "info": 20 | return SeverityINFO 21 | case "medium": 22 | return SeverityMEDIUM 23 | case "high": 24 | return SeverityHIGH 25 | case "critical": 26 | return SeverityCRITICAL 27 | default: 28 | return SeverityUnknown 29 | } 30 | } 31 | 32 | var SeverityMap = map[int]string{ 33 | SeverityINFO: "info", 34 | SeverityMEDIUM: "medium", 35 | SeverityHIGH: "high", 36 | SeverityCRITICAL: "critical", 37 | } 38 | 39 | type Vuln struct { 40 | Name string `json:"name"` 41 | Tags []string `json:"tags,omitempty"` 42 | Payload map[string]interface{} `json:"payload,omitempty"` 43 | Detail map[string][]string `json:"detail,omitempty"` 44 | SeverityLevel int `json:"severity"` 45 | Framework *Framework `json:"-"` 46 | } 47 | 48 | func (v *Vuln) HasTag(tag string) bool { 49 | for _, t := range v.Tags { 50 | if t == tag { 51 | return true 52 | } 53 | } 54 | return false 55 | } 56 | 57 | func (v *Vuln) GetPayload() string { 58 | return iutils.MapToString(v.Payload) 59 | } 60 | 61 | func (v *Vuln) GetDetail() string { 62 | var s strings.Builder 63 | for k, v := range v.Detail { 64 | s.WriteString(fmt.Sprintf(" %s:%s ", k, strings.Join(v, ","))) 65 | } 66 | return s.String() 67 | } 68 | 69 | func (v *Vuln) String() string { 70 | s := v.Name 71 | if payload := v.GetPayload(); payload != "" { 72 | s += fmt.Sprintf(" payloads:%s", iutils.AsciiEncode(payload)) 73 | } 74 | if detail := v.GetDetail(); detail != "" { 75 | s += fmt.Sprintf(" payloads:%s", iutils.AsciiEncode(detail)) 76 | } 77 | return s 78 | } 79 | 80 | type Vulns map[string]*Vuln 81 | 82 | func (vs Vulns) One() *Vuln { 83 | for _, v := range vs { 84 | return v 85 | } 86 | return nil 87 | } 88 | 89 | func (vs Vulns) List() []*Vuln { 90 | var vulns []*Vuln 91 | for _, v := range vs { 92 | vulns = append(vulns, v) 93 | } 94 | return vulns 95 | } 96 | 97 | func (vs Vulns) Add(other *Vuln) bool { 98 | if _, ok := vs[other.Name]; !ok { 99 | vs[other.Name] = other 100 | return true 101 | } 102 | return false 103 | } 104 | 105 | func (vs Vulns) String() string { 106 | var s string 107 | 108 | for _, vuln := range vs { 109 | s += fmt.Sprintf("[ %s: %s ] ", SeverityMap[vuln.SeverityLevel], vuln.String()) 110 | } 111 | return s 112 | } 113 | 114 | func (vs Vulns) Merge(other Vulns) int { 115 | // name, tag 统一小写, 减少指纹库之间的差异 116 | var n int 117 | for _, v := range other { 118 | v.Name = strings.ToLower(v.Name) 119 | if frame, ok := vs[v.Name]; ok { 120 | if len(v.Tags) > 0 { 121 | for i, tag := range v.Tags { 122 | v.Tags[i] = strings.ToLower(tag) 123 | } 124 | frame.Tags = iutils.StringsUnique(append(frame.Tags, v.Tags...)) 125 | } 126 | } else { 127 | vs[v.Name] = v 128 | n += n 129 | } 130 | } 131 | return n 132 | } 133 | 134 | func (vs Vulns) HasTag(tag string) bool { 135 | for _, f := range vs { 136 | if f.HasTag(tag) { 137 | return true 138 | } 139 | } 140 | return false 141 | } 142 | -------------------------------------------------------------------------------- /nmap/data.go: -------------------------------------------------------------------------------- 1 | package gonmap 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // NmapProbesData 用于 JSON/YAML 序列化和反序列化的顶层数据结构 9 | type NmapProbesData struct { 10 | Probes []*Probe `json:"probes" yaml:"probes"` 11 | Services map[string]string `json:"services,omitempty" yaml:"services,omitempty"` 12 | } 13 | 14 | // LoadFromJSON 从 JSON 数据加载探针数据 15 | func (data *NmapProbesData) LoadFromJSON(jsonData []byte) error { 16 | // 这个方法将在后续实现,用于从预处理的 JSON 数据中加载探针 17 | // 避免每次都解析原始的 nmap-service-probes 文件 18 | return nil 19 | } 20 | 21 | // ExportProbes 导出当前加载的探针数据为可序列化的格式 22 | func ExportProbes() *NmapProbesData { 23 | // 从全局nmap实例导出探针数据 24 | probes := make([]*Probe, 0, len(nmap.probeNameMap)) 25 | 26 | // 将probeNameMap中的探针转换为数组 27 | for _, probe := range nmap.probeNameMap { 28 | // 创建探针副本,避免引用问题 29 | probeCopy := *probe 30 | probes = append(probes, &probeCopy) 31 | } 32 | 33 | return &NmapProbesData{ 34 | Probes: probes, 35 | Services: copyNmapServices(), 36 | } 37 | } 38 | 39 | // copyNmapServices 复制nmap服务映射 40 | func copyNmapServices() map[string]string { 41 | services := make(map[string]string) 42 | 43 | // 确保nmap已经初始化 44 | if nmap == nil { 45 | initNmap() 46 | } 47 | 48 | // 将nmapServices数组转换为map格式 49 | if nmap.nmapServices != nil { 50 | for port, service := range nmap.nmapServices { 51 | if service != "" && service != "unknown" { 52 | services[fmt.Sprintf("%d", port)] = service 53 | } 54 | } 55 | } 56 | 57 | return services 58 | } 59 | 60 | // TempNmapParser 临时的nmap解析器,用于transform工具 61 | type TempNmapParser struct { 62 | probeNameMap map[string]*Probe 63 | } 64 | 65 | // NewTempParser 创建临时解析器 66 | func NewTempParser(content string) *TempNmapParser { 67 | parser := &TempNmapParser{ 68 | probeNameMap: make(map[string]*Probe), 69 | } 70 | parser.loads(content) 71 | return parser 72 | } 73 | 74 | // GetProbes 获取解析的探针 75 | func (t *TempNmapParser) GetProbes() map[string]*Probe { 76 | return t.probeNameMap 77 | } 78 | 79 | // loads 解析nmap-service-probes内容(从type-nmap.go复制) 80 | func (t *TempNmapParser) loads(s string) { 81 | lines := strings.Split(s, "\n") 82 | var probeGroups [][]string 83 | var probeLines []string 84 | for _, line := range lines { 85 | if !t.isCommand(line) { 86 | continue 87 | } 88 | commandName := line[:strings.Index(line, " ")] 89 | if commandName == "Exclude" { 90 | continue // 忽略Exclude命令 91 | } 92 | if commandName == "Probe" { 93 | if len(probeLines) != 0 { 94 | probeGroups = append(probeGroups, probeLines) 95 | probeLines = []string{} 96 | } 97 | } 98 | probeLines = append(probeLines, line) 99 | } 100 | probeGroups = append(probeGroups, probeLines) 101 | 102 | for _, lines := range probeGroups { 103 | p := parseProbe(lines) 104 | t.pushProbe(*p) 105 | } 106 | } 107 | 108 | // pushProbe 添加探针到映射中 109 | func (t *TempNmapParser) pushProbe(p Probe) { 110 | t.probeNameMap[p.Name] = &p 111 | } 112 | 113 | // isCommand 检查是否是有效命令行(从type-nmap.go复制) 114 | func (t *TempNmapParser) isCommand(line string) bool { 115 | //删除注释行和空行 116 | if len(line) < 2 { 117 | return false 118 | } 119 | if line[:1] == "#" { 120 | return false 121 | } 122 | //删除异常命令 123 | commandName := line[:strings.Index(line, " ")] 124 | commandArr := []string{ 125 | "Exclude", "Probe", "match", "softmatch", "ports", "sslports", "totalwaitms", "tcpwrappedms", "rarity", "fallback", 126 | } 127 | for _, item := range commandArr { 128 | if item == commandName { 129 | return true 130 | } 131 | } 132 | return false 133 | } 134 | -------------------------------------------------------------------------------- /fingers/common.go: -------------------------------------------------------------------------------- 1 | package fingers 2 | 3 | import ( 4 | "bytes" 5 | "github.com/chainreactors/fingers/common" 6 | "github.com/chainreactors/fingers/resources" 7 | "github.com/chainreactors/utils/iutils" 8 | ) 9 | 10 | func NewContent(c []byte, cert string, ishttp bool) *Content { 11 | content := &Content{ 12 | Cert: cert, 13 | } 14 | c = iutils.UTF8ConvertBytes(c) 15 | if ishttp { 16 | content.UpdateContent(c) 17 | } else { 18 | content.Content = c 19 | } 20 | return content 21 | } 22 | 23 | type Content struct { 24 | Content []byte `json:"content"` 25 | Header []byte `json:"header"` 26 | Body []byte `json:"body"` 27 | Cert string `json:"cert"` 28 | } 29 | 30 | func (c *Content) UpdateContent(content []byte) { 31 | c.Content = bytes.ToLower(content) 32 | cs := bytes.Index(c.Content, []byte("\r\n\r\n")) 33 | if cs != -1 { 34 | c.Body = c.Content[cs+4:] 35 | c.Header = c.Content[:cs] 36 | } 37 | } 38 | 39 | // LoadFingers 加载指纹 迁移到fingers包, 允许其他服务调用 40 | func LoadFingers(content []byte) (fingers Fingers, err error) { 41 | err = resources.UnmarshalData(content, &fingers) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return fingers, nil 47 | } 48 | 49 | type FingerMapper map[string]Fingers 50 | 51 | type Fingers []*Finger 52 | 53 | func (fs Fingers) GroupByPort() FingerMapper { 54 | fingermap := make(FingerMapper) 55 | for _, f := range fs { 56 | if f.DefaultPort != nil { 57 | for _, port := range resources.PrePort.ParsePortSlice(f.DefaultPort) { 58 | fingermap[port] = append(fingermap[port], f) 59 | } 60 | } else { 61 | fingermap["0"] = append(fingermap["0"], f) 62 | } 63 | } 64 | return fingermap 65 | } 66 | 67 | func (fs Fingers) GroupByMod() (Fingers, Fingers) { 68 | var active, passive Fingers 69 | for _, f := range fs { 70 | if f.IsActive { 71 | active = append(active, f) 72 | } else { 73 | passive = append(passive, f) 74 | } 75 | } 76 | return active, passive 77 | } 78 | 79 | func (fs Fingers) PassiveMatch(input *Content, stopAtFirst bool) (common.Frameworks, common.Vulns) { 80 | frames := make(common.Frameworks) 81 | vulns := make(common.Vulns) 82 | for _, finger := range fs { 83 | // sender置空, 所有的发包交给spray的pool 84 | frame, vuln, ok := finger.PassiveMatch(input) 85 | if ok { 86 | frames.Add(frame) 87 | if vuln != nil { 88 | vulns[vuln.Name] = vuln 89 | } 90 | if stopAtFirst { 91 | break 92 | } 93 | } 94 | } 95 | return frames, vulns 96 | } 97 | 98 | func (fs Fingers) ActiveMatch(level int, sender Sender, callback Callback, stopAtFirst bool) (common.Frameworks, common.Vulns) { 99 | frames := make(common.Frameworks) 100 | vulns := make(common.Vulns) 101 | for _, finger := range fs { 102 | frame, vuln, ok := finger.ActiveMatch(level, sender) 103 | if callback != nil { 104 | callback(frame, vuln) 105 | } 106 | if ok { 107 | frames.Add(frame) 108 | if vuln != nil { 109 | vulns[vuln.Name] = vuln 110 | } 111 | if stopAtFirst { 112 | break 113 | } 114 | } 115 | } 116 | return frames, vulns 117 | } 118 | 119 | func (fs Fingers) Match(input *Content, level int, sender Sender, callback Callback, stopAtFirst bool) (common.Frameworks, common.Vulns) { 120 | frames := make(common.Frameworks) 121 | vulns := make(common.Vulns) 122 | for _, finger := range fs { 123 | frame, vuln, ok := finger.Match(input, level, sender) 124 | if callback != nil { 125 | callback(frame, vuln) 126 | } 127 | if ok { 128 | ok = true 129 | frames.Add(frame) 130 | if vuln != nil { 131 | vulns.Add(vuln) 132 | } 133 | if stopAtFirst { 134 | break 135 | } 136 | } 137 | } 138 | return frames, vulns 139 | } 140 | -------------------------------------------------------------------------------- /nmap/type-fingerprint.go: -------------------------------------------------------------------------------- 1 | package gonmap 2 | 3 | import "github.com/chainreactors/fingers/common" 4 | 5 | type FingerPrint struct { 6 | ProbeName string `json:"probe_name,omitempty"` 7 | MatchRegexString string `json:"match_regex,omitempty"` 8 | 9 | Service string `json:"service,omitempty"` 10 | ProductName string `json:"product_name,omitempty"` 11 | Version string `json:"version,omitempty"` 12 | Info string `json:"info,omitempty"` 13 | Hostname string `json:"hostname,omitempty"` 14 | OperatingSystem string `json:"operating_system,omitempty"` 15 | DeviceType string `json:"device_type,omitempty"` 16 | 17 | // CPE信息,支持多个CPE条目 18 | CPEs []string `json:"cpes,omitempty"` 19 | // 解析后的CPE属性 20 | CPEAttributes []*common.Attributes `json:"cpe_attributes,omitempty"` 21 | // p/vendorproductname/ 22 | // v/version/ 23 | // i/info/ 24 | // h/hostname/ 25 | // o/operatingsystem/ 26 | // d/devicetype/ 27 | // c/cpe/ - CPE信息 28 | } 29 | 30 | // ToFrameworks 将FingerPrint转换为多个common.Framework,支持多个CPE app 31 | func (fp *FingerPrint) ToFrameworks() []*common.Framework { 32 | if fp.Service == "" { 33 | return nil 34 | } 35 | 36 | var frameworks []*common.Framework 37 | 38 | // 修复协议名称 39 | service := FixProtocol(fp.Service) 40 | 41 | // 如果有CPE app类型的属性,为每个创建一个Framework 42 | appCPEAttrs := fp.getAppCPEAttributes() 43 | 44 | if len(appCPEAttrs) > 0 { 45 | // 为每个app类型的CPE创建Framework 46 | for _, attr := range appCPEAttrs { 47 | framework := common.NewFramework(service, common.FrameFromNmap) 48 | fp.populateFramework(framework, attr) 49 | frameworks = append(frameworks, framework) 50 | } 51 | } else { 52 | // 没有CPE app信息,创建基本的Framework 53 | framework := common.NewFramework(service, common.FrameFromNmap) 54 | fp.populateFramework(framework, nil) 55 | frameworks = append(frameworks, framework) 56 | } 57 | 58 | return frameworks 59 | } 60 | 61 | // getAppCPEAttributes 获取所有app类型的CPE属性 62 | func (fp *FingerPrint) getAppCPEAttributes() []*common.Attributes { 63 | var appAttrs []*common.Attributes 64 | for _, attr := range fp.CPEAttributes { 65 | if attr != nil && attr.Part == "a" { // 只关心application类型 66 | appAttrs = append(appAttrs, attr) 67 | } 68 | } 69 | return appAttrs 70 | } 71 | 72 | // populateFramework 填充Framework的信息 73 | func (fp *FingerPrint) populateFramework(framework *common.Framework, cpeAttr *common.Attributes) { 74 | // 优先使用CPE属性,其次使用FingerPrint属性 75 | if cpeAttr != nil { 76 | // 使用CPE中的产品和版本信息 77 | if cpeAttr.Product != "" { 78 | framework.Product = cpeAttr.Product 79 | } 80 | if cpeAttr.Version != "" { 81 | framework.Version = cpeAttr.Version 82 | } 83 | // 将厂商信息添加到标签中 84 | if cpeAttr.Vendor != "" { 85 | framework.Tags = append(framework.Tags, "vendor:"+cpeAttr.Vendor) 86 | } 87 | } else { 88 | // 使用FingerPrint中的基本信息 89 | if fp.ProductName != "" { 90 | framework.Product = fp.ProductName 91 | } 92 | if fp.Version != "" { 93 | framework.Version = fp.Version 94 | } 95 | } 96 | 97 | // 添加其他信息到标签 98 | if fp.Info != "" { 99 | framework.Tags = append(framework.Tags, fp.Info) 100 | } 101 | if fp.Hostname != "" { 102 | framework.Tags = append(framework.Tags, "hostname:"+fp.Hostname) 103 | } 104 | if fp.DeviceType != "" { 105 | framework.Tags = append(framework.Tags, "device:"+fp.DeviceType) 106 | } 107 | 108 | // 将所有CPE字符串添加到标签中(只保留app类型的) 109 | for i, cpe := range fp.CPEs { 110 | if i < len(fp.CPEAttributes) && fp.CPEAttributes[i] != nil && fp.CPEAttributes[i].Part == "a" { 111 | framework.Tags = append(framework.Tags, "cpe:"+cpe) 112 | } 113 | } 114 | 115 | // 标记为主动扫描结果 116 | framework.Froms = map[common.From]bool{common.FrameFromACTIVE: true} 117 | } 118 | -------------------------------------------------------------------------------- /cmd/nmap/README.md: -------------------------------------------------------------------------------- 1 | # Nmap指纹扫描命令行工具 2 | 3 | 这是一个基于chainreactors/fingers项目中nmap引擎的并发指纹扫描命令行工具。 4 | 5 | ## 功能特性 6 | 7 | - ✅ 支持CIDR网段和单个IP地址扫描 8 | - ✅ 支持端口范围和单个端口扫描 9 | - ✅ 高并发扫描,可配置线程数 10 | - ✅ 基于nmap指纹库进行服务识别 11 | - ✅ 实时进度显示和统计信息 12 | - ✅ 详细的扫描结果输出 13 | - ✅ 可配置扫描深度级别 14 | 15 | ## 安装和编译 16 | 17 | ```bash 18 | # 进入项目目录 19 | cd cmd/nmap 20 | 21 | # 编译 22 | go build -o nmap-scanner . 23 | 24 | # 或者直接运行 25 | go run . 26 | ``` 27 | 28 | ## 使用方法 29 | 30 | ### 基本用法 31 | 32 | ```bash 33 | # 扫描单个IP的常见端口 34 | ./nmap-scanner -cidr 192.168.1.1 -port 22,80,443 35 | 36 | # 扫描整个C段的HTTP服务 37 | ./nmap-scanner -cidr 192.168.1.0/24 -port 80,443,8080 38 | 39 | # 扫描端口范围 40 | ./nmap-scanner -cidr 10.0.0.1-10.0.0.10 -port 1000-2000 41 | ``` 42 | 43 | ### 高级用法 44 | 45 | ```bash 46 | # 高并发扫描(200线程) 47 | ./nmap-scanner -cidr 192.168.1.0/24 -port 22,80,443,3389 -threads 200 48 | 49 | # 设置超时时间(5秒) 50 | ./nmap-scanner -cidr 192.168.1.1 -port 80 -timeout 5 51 | 52 | # 高深度扫描(使用更多探针) 53 | ./nmap-scanner -cidr 192.168.1.1 -port 80 -level 6 54 | 55 | # 详细输出模式 56 | ./nmap-scanner -cidr 192.168.1.1 -port 80 -v 57 | 58 | # 保存结果到文件 59 | ./nmap-scanner -cidr 192.168.1.0/24 -port 80,443 -o results.txt 60 | ``` 61 | 62 | ## 参数说明 63 | 64 | | 参数 | 说明 | 默认值 | 示例 | 65 | |------|------|--------|------| 66 | | `-cidr` | 目标CIDR范围或IP地址 | `127.0.0.1/32` | `192.168.1.0/24` | 67 | | `-port` | 端口范围,支持逗号分隔和范围 | `22,80,443` | `80,443,1000-2000` | 68 | | `-threads` | 并发线程数 | `100` | `200` | 69 | | `-timeout` | 扫描超时时间(秒) | `3` | `5` | 70 | | `-level` | 扫描深度级别(1-9) | `1` | `6` | 71 | | `-v` | 详细输出模式 | `false` | `-v` | 72 | | `-o` | 输出文件路径 | 无 | `results.txt` | 73 | 74 | ## 输出格式 75 | 76 | ### 扫描结果 77 | 78 | ``` 79 | ✅ 192.168.1.1:80 -> nginx [nginx] v1.18.0 [http, web-server] 80 | ✅ 192.168.1.1:443 -> nginx [nginx] v1.18.0 [https, web-server, tls] 81 | 🔍 192.168.1.1:22 -> 已识别但无详细信息 82 | 🔓 192.168.1.1:3389 -> 端口开放 83 | ``` 84 | 85 | ### 进度信息 86 | 87 | ``` 88 | 📈 进度: 45.2% (1810/4000) | 速度: 156.3/s | 开放: 23 | 识别: 15 89 | ``` 90 | 91 | ### 最终统计 92 | 93 | ``` 94 | ✅ 扫描完成! 95 | 总耗时: 25.6s 96 | 扫描目标: 4000 97 | 开放端口: 23 98 | 识别服务: 15 99 | 扫描速度: 156.25 targets/sec 100 | ``` 101 | 102 | ## 支持的端口格式 103 | 104 | - 单个端口:`80` 105 | - 多个端口:`80,443,8080` 106 | - 端口范围:`1000-2000` 107 | - 混合格式:`22,80,443,1000-2000,8080-8090` 108 | 109 | ## 支持的CIDR格式 110 | 111 | - 单个IP:`192.168.1.1` 112 | - CIDR网段:`192.168.1.0/24` 113 | - 大网段:`10.0.0.0/16` 114 | 115 | ## 扫描级别说明 116 | 117 | | 级别 | 描述 | 探针数量 | 适用场景 | 118 | |------|------|----------|----------| 119 | | 1 | 快速扫描 | 基础探针 | 快速发现常见服务 | 120 | | 3 | 标准扫描 | 常用探针 | 平衡速度和准确性 | 121 | | 6 | 深度扫描 | 大部分探针 | 详细服务识别 | 122 | | 9 | 完全扫描 | 所有探针 | 最全面的识别 | 123 | 124 | ## 性能优化建议 125 | 126 | 1. **网络环境**:在良好的网络环境下使用更高的并发数 127 | 2. **目标规模**:大规模扫描时适当降低并发数避免网络拥塞 128 | 3. **超时设置**:根据网络延迟调整超时时间 129 | 4. **扫描级别**:根据需求选择合适的扫描级别 130 | 131 | ## 使用示例 132 | 133 | ### 内网资产发现 134 | 135 | ```bash 136 | # 发现内网Web服务 137 | ./nmap-scanner -cidr 192.168.0.0/16 -port 80,443,8080,8443 -threads 300 138 | 139 | # 发现内网常见服务 140 | ./nmap-scanner -cidr 10.0.0.0/8 -port 22,80,443,3389,1433,3306 -threads 200 141 | ``` 142 | 143 | ### 单机服务识别 144 | 145 | ```bash 146 | # 详细识别单台主机服务 147 | ./nmap-scanner -cidr 192.168.1.100 -port 1-65535 -level 6 -v 148 | ``` 149 | 150 | ### 快速端口扫描 151 | 152 | ```bash 153 | # 快速扫描常见端口 154 | ./nmap-scanner -cidr 192.168.1.0/24 -port 22,80,443,3389 -threads 500 -timeout 2 155 | ``` 156 | 157 | ## 注意事项 158 | 159 | 1. 请确保有合法的扫描授权 160 | 2. 大规模扫描可能触发防火墙或IDS告警 161 | 3. 建议在测试环境中先验证扫描参数 162 | 4. 高并发扫描时注意系统资源使用情况 163 | 164 | ## 技术实现 165 | 166 | - 基于chainreactors/fingers项目的nmap指纹引擎 167 | - 使用goroutines实现高并发扫描 168 | - 支持TCP连接和指纹匹配 169 | - 实时统计和进度显示 170 | - 内置CIDR和端口解析功能 171 | 172 | ## 故障排除 173 | 174 | ### 常见问题 175 | 176 | 1. **连接超时**:增加timeout参数值 177 | 2. **扫描速度慢**:增加threads参数值 178 | 3. **内存使用过高**:降低threads参数值 179 | 4. **识别率低**:增加level参数值 180 | 181 | ### 调试模式 182 | 183 | ```bash 184 | # 启用详细输出查看更多信息 185 | ./nmap-scanner -cidr 192.168.1.1 -port 80 -v -level 6 186 | ``` 187 | 188 | -------------------------------------------------------------------------------- /wappalyzer/fingerprint_body.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | "bytes" 5 | "github.com/chainreactors/fingers/common" 6 | "unsafe" 7 | 8 | "golang.org/x/net/html" 9 | ) 10 | 11 | // checkBody checks for fingerprints in the HTML body 12 | func (engine *Wappalyze) checkBody(body []byte) common.Frameworks { 13 | technologies := make(common.Frameworks) 14 | bodyString := unsafeToString(body) 15 | technologies.Merge(engine.fingerprints.matchString(bodyString, htmlPart)) 16 | 17 | // Tokenize the HTML document and check for fingerprints as required 18 | tokenizer := html.NewTokenizer(bytes.NewReader(body)) 19 | 20 | for { 21 | tt := tokenizer.Next() 22 | switch tt { 23 | case html.ErrorToken: 24 | return technologies 25 | case html.StartTagToken: 26 | token := tokenizer.Token() 27 | switch token.Data { 28 | case "script": 29 | // Check if the script tag has a source file to check 30 | source, found := getScriptSource(token) 31 | if found { 32 | // Check the script tags for script fingerprints 33 | technologies.Merge(engine.fingerprints.matchString(source, scriptPart)) 34 | continue 35 | } 36 | 37 | // Check the text attribute of the tag for javascript based technologies. 38 | // The next token should be the contents of the script tag 39 | if tokenType := tokenizer.Next(); tokenType != html.TextToken { 40 | continue 41 | } 42 | 43 | // TODO: JS requires a running VM, for checking properties. Only 44 | // possible with headless for now :( 45 | 46 | // data := tokenizer.Token().Data 47 | // technologies = append( 48 | // technologies, 49 | // s.fingerprints.matchString(data, jsPart)..., 50 | // ) 51 | case "meta": 52 | // For meta tag, we are only interested in name and content attributes. 53 | name, content, found := getMetaNameAndContent(token) 54 | if !found { 55 | continue 56 | } 57 | technologies.Merge(engine.fingerprints.matchKeyValueString(name, content, metaPart)) 58 | } 59 | case html.SelfClosingTagToken: 60 | token := tokenizer.Token() 61 | if token.Data != "meta" { 62 | continue 63 | } 64 | 65 | // Parse the meta tag and check for tech 66 | name, content, found := getMetaNameAndContent(token) 67 | if !found { 68 | continue 69 | } 70 | technologies.Merge(engine.fingerprints.matchKeyValueString(name, content, metaPart)) 71 | } 72 | } 73 | } 74 | 75 | func (engine *Wappalyze) getTitle(body []byte) string { 76 | var title string 77 | 78 | // Tokenize the HTML document and check for fingerprints as required 79 | tokenizer := html.NewTokenizer(bytes.NewReader(body)) 80 | 81 | for { 82 | tt := tokenizer.Next() 83 | switch tt { 84 | case html.ErrorToken: 85 | return title 86 | case html.StartTagToken: 87 | token := tokenizer.Token() 88 | switch token.Data { 89 | case "title": 90 | // Next text token will be the actual title of the page 91 | if tokenType := tokenizer.Next(); tokenType != html.TextToken { 92 | continue 93 | } 94 | title = tokenizer.Token().Data 95 | } 96 | } 97 | } 98 | } 99 | 100 | // getMetaNameAndContent gets name and content attributes from meta html token 101 | func getMetaNameAndContent(token html.Token) (string, string, bool) { 102 | if len(token.Attr) < keyValuePairLength { 103 | return "", "", false 104 | } 105 | 106 | var name, content string 107 | for _, attr := range token.Attr { 108 | switch attr.Key { 109 | case "name": 110 | name = attr.Val 111 | case "content": 112 | content = attr.Val 113 | } 114 | } 115 | return name, content, true 116 | } 117 | 118 | // getScriptSource gets src tag from a script tag 119 | func getScriptSource(token html.Token) (string, bool) { 120 | if len(token.Attr) < 1 { 121 | return "", false 122 | } 123 | 124 | var source string 125 | for _, attr := range token.Attr { 126 | switch attr.Key { 127 | case "src": 128 | source = attr.Val 129 | } 130 | } 131 | return source, true 132 | } 133 | 134 | // unsafeToString converts a byte slice to string and does it with 135 | // zero allocations. 136 | // 137 | // NOTE: This function should only be used if its certain that the underlying 138 | // array has not been manipulated. 139 | // 140 | // Reference - https://github.com/golang/go/issues/25484 141 | func unsafeToString(data []byte) string { 142 | return *(*string)(unsafe.Pointer(&data)) 143 | } 144 | -------------------------------------------------------------------------------- /nmap/engine.go: -------------------------------------------------------------------------------- 1 | package gonmap 2 | 3 | import ( 4 | "github.com/chainreactors/fingers/common" 5 | ) 6 | 7 | type NmapEngine struct { 8 | nmap *Nmap 9 | } 10 | 11 | // NewNmapEngine 创建新的 nmap 引擎实例 12 | func NewNmapEngine(probesData, servicesData []byte) (*NmapEngine, error) { 13 | // 手动初始化nmap实例,传入数据 14 | n := NewWithData(probesData, servicesData) 15 | 16 | return &NmapEngine{ 17 | nmap: n, 18 | }, nil 19 | } 20 | 21 | // Name 实现 EngineImpl 接口 22 | func (e *NmapEngine) Name() string { 23 | return "nmap" 24 | } 25 | 26 | // Compile 实现 EngineImpl 接口 27 | func (e *NmapEngine) Compile() error { 28 | // gonmap 在 init() 中已经完成编译,这里不需要额外操作 29 | return nil 30 | } 31 | 32 | // Len 实现 EngineImpl 接口 33 | func (e *NmapEngine) Len() int { 34 | // 返回 nmap 指纹库的总指纹数 35 | return len(e.nmap.probeNameMap) 36 | } 37 | 38 | // WebMatch 实现Web指纹匹配 - nmap不支持Web指纹 39 | func (e *NmapEngine) WebMatch(content []byte) common.Frameworks { 40 | // nmap不支持Web指纹识别 41 | return make(common.Frameworks) 42 | } 43 | 44 | // ServiceMatch 实现Service指纹匹配 45 | func (e *NmapEngine) ServiceMatch(host string, portStr string, level int, sender common.ServiceSender, callback common.ServiceCallback) *common.ServiceResult { 46 | if sender == nil || level <= 0 { 47 | return nil 48 | } 49 | 50 | // 创建适配器将common.ServiceSender转换为nmap内部sender格式 51 | nmapSender := func(host string, port int, data []byte, requestTLS bool) ([]byte, bool, error) { 52 | // 根据TLS需求和端口特性选择网络协议 53 | network := "tcp" 54 | if requestTLS || isHTTPSPort(port) { 55 | network = "tls" 56 | } 57 | 58 | // 直接使用端口字符串,让ServiceSender处理UDP等协议前缀 59 | response, err := sender.Send(host, portStr, data, network) 60 | if err != nil { 61 | // 如果TLS失败,尝试普通TCP 62 | if network == "tls" { 63 | response, err = sender.Send(host, portStr, data, "tcp") 64 | if err == nil { 65 | return response, false, nil // 成功但不是TLS 66 | } 67 | } 68 | return nil, false, err 69 | } 70 | 71 | // 返回响应和实际使用的协议类型 72 | actualTLS := (network == "tls") 73 | return response, actualTLS, nil 74 | } 75 | 76 | // 解析端口字符串获取端口号(用于其他逻辑) 77 | portNum, _, _ := e.nmap.parsePortString(portStr) 78 | 79 | // 使用nmap的完整扫描逻辑,但网络发送由外部sender控制 80 | status, response := e.nmap.Scan(host, portStr, level, nmapSender) 81 | 82 | 83 | var framework *common.Framework 84 | 85 | if status == Matched && response != nil && response.FingerPrint != nil { 86 | // 扫描成功,获取多个Framework(支持多个CPE app) 87 | frameworks := response.FingerPrint.ToFrameworks() 88 | if len(frameworks) > 0 { 89 | framework = frameworks[0] // 取第一个Framework作为主要结果 90 | } 91 | } else if status == Open { 92 | // 端口开放但无法识别服务,使用guess功能猜测服务 93 | guessedProtocol := GuessProtocol(portNum) 94 | if guessedProtocol != "" && guessedProtocol != "unknown" { 95 | // 创建基于猜测的Framework 96 | framework = common.NewFramework(FixProtocol(guessedProtocol), common.FrameFromGUESS) 97 | // 添加guess标记 98 | framework.Tags = append(framework.Tags, "guess") 99 | } 100 | } 101 | // 如果status是Closed或其他状态,framework保持为nil,表示端口未开放或无法连接 102 | 103 | if framework == nil { 104 | return nil 105 | } 106 | 107 | result := &common.ServiceResult{ 108 | Framework: framework, 109 | Vuln: nil, // nmap一般不直接返回漏洞信息 110 | } 111 | 112 | // 调用回调函数 113 | if callback != nil { 114 | callback(result) 115 | } 116 | 117 | return result 118 | } 119 | 120 | // matchResponse 使用nmap指纹库分析响应数据 121 | func (e *NmapEngine) matchResponse(responseData []byte, host string, port int) *common.Framework { 122 | // 使用nmap的指纹匹配逻辑 123 | // 调用nmap的核心指纹识别函数,不涉及网络请求 124 | responseStr := string(responseData) 125 | fingerPrint := e.nmap.getFinger(responseStr, false, "") 126 | 127 | if fingerPrint != nil && fingerPrint.Service != "" { 128 | frameworks := fingerPrint.ToFrameworks() 129 | if len(frameworks) > 0 { 130 | return frameworks[0] // 返回第一个Framework 131 | } 132 | } 133 | return nil 134 | } 135 | 136 | // Capability 实现 EngineImpl 接口 - 返回引擎能力 137 | func (e *NmapEngine) Capability() common.EngineCapability { 138 | return common.EngineCapability{ 139 | SupportWeb: false, // nmap不支持Web指纹 140 | SupportService: true, // nmap支持Service指纹 141 | } 142 | } 143 | 144 | // isHTTPSPort 判断是否是常见的HTTPS端口 145 | func isHTTPSPort(port int) bool { 146 | httpsports := []int{443, 8443, 993, 995, 465, 636, 989, 990, 992, 993, 994, 995, 5986} 147 | for _, p := range httpsports { 148 | if port == p { 149 | return true 150 | } 151 | } 152 | return false 153 | } 154 | -------------------------------------------------------------------------------- /common/sender.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // Service指纹检测的Sender抽象 13 | type ServiceSender interface { 14 | Send(host string, portStr string, data []byte, network string) ([]byte, error) 15 | } 16 | 17 | // DefaultServiceSender 默认的ServiceSender实现 18 | type DefaultServiceSender struct { 19 | timeout time.Duration 20 | } 21 | 22 | // NewServiceSender 创建默认的ServiceSender 23 | func NewServiceSender(timeout time.Duration) ServiceSender { 24 | if timeout <= 0 { 25 | timeout = 5 * time.Second // 默认5秒超时 26 | } 27 | return &DefaultServiceSender{ 28 | timeout: timeout, 29 | } 30 | } 31 | 32 | // Send 实现ServiceSender接口,支持TCP、UDP、TLS协议 33 | func (d *DefaultServiceSender) Send(host string, portStr string, data []byte, network string) ([]byte, error) { 34 | // 解析端口字符串 35 | port, actualNetwork := d.parsePortString(portStr, network) 36 | target := fmt.Sprintf("%s:%d", host, port) 37 | 38 | // 使用解析后的网络协议类型 39 | switch strings.ToLower(actualNetwork) { 40 | case "tls", "ssl": 41 | return d.sendTLS(target, data) 42 | case "udp": 43 | return d.sendUDP(target, data) 44 | case "tcp", "": 45 | return d.sendTCP(target, data) 46 | default: 47 | return d.sendTCP(target, data) 48 | } 49 | } 50 | 51 | // sendTCP 发送TCP数据 52 | func (d *DefaultServiceSender) sendTCP(target string, data []byte) ([]byte, error) { 53 | conn, err := net.DialTimeout("tcp", target, d.timeout) 54 | if err != nil { 55 | return nil, err 56 | } 57 | defer conn.Close() 58 | 59 | // 发送数据 60 | if len(data) > 0 { 61 | // 设置写超时 62 | conn.SetWriteDeadline(time.Now().Add(d.timeout)) 63 | _, err = conn.Write(data) 64 | if err != nil { 65 | return nil, err 66 | } 67 | } 68 | 69 | // 使用完整的timeout时间,不再强制限制 70 | readTimeout := d.timeout 71 | conn.SetReadDeadline(time.Now().Add(readTimeout)) 72 | 73 | // 读取响应 - 改进错误处理,即使连接被关闭也要返回已读取的数据 74 | buffer := make([]byte, 10240) 75 | n, err := conn.Read(buffer) 76 | 77 | // 即使有错误,只要读取到了数据就返回数据 78 | // 这对于SMB/RDP等协议很重要,它们可能在发送响应后立即关闭连接 79 | if n > 0 { 80 | return buffer[:n], nil 81 | } 82 | 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | return buffer[:n], nil 88 | } 89 | 90 | // sendTLS 发送TLS数据 91 | func (d *DefaultServiceSender) sendTLS(target string, data []byte) ([]byte, error) { 92 | conn, err := tls.DialWithDialer(&net.Dialer{ 93 | Timeout: d.timeout, 94 | }, "tcp", target, &tls.Config{ 95 | InsecureSkipVerify: true, 96 | }) 97 | if err != nil { 98 | return nil, err 99 | } 100 | defer conn.Close() 101 | 102 | // 发送数据 103 | if len(data) > 0 { 104 | // 设置写超时 105 | conn.SetWriteDeadline(time.Now().Add(d.timeout)) 106 | _, err = conn.Write(data) 107 | if err != nil { 108 | return nil, err 109 | } 110 | } 111 | 112 | // 使用完整的timeout时间,不再强制限制 113 | readTimeout := d.timeout 114 | conn.SetReadDeadline(time.Now().Add(readTimeout)) 115 | 116 | // 读取响应 - 改进错误处理,即使连接被关闭也要返回已读取的数据 117 | buffer := make([]byte, 10240) 118 | n, err := conn.Read(buffer) 119 | 120 | // 即使有错误,只要读取到了数据就返回数据 121 | // 这对于SMB/RDP等协议很重要,它们可能在发送响应后立即关闭连接 122 | if n > 0 { 123 | return buffer[:n], nil 124 | } 125 | 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | return buffer[:n], nil 131 | } 132 | 133 | // sendUDP 发送UDP数据 134 | func (d *DefaultServiceSender) sendUDP(target string, data []byte) ([]byte, error) { 135 | conn, err := net.DialTimeout("udp", target, d.timeout) 136 | if err != nil { 137 | return nil, err 138 | } 139 | defer conn.Close() 140 | 141 | // 发送数据 142 | if len(data) > 0 { 143 | // 设置写超时 144 | conn.SetWriteDeadline(time.Now().Add(d.timeout)) 145 | _, err = conn.Write(data) 146 | if err != nil { 147 | return nil, err 148 | } 149 | } 150 | 151 | // UDP通常响应更快,设置更短的读超时 152 | readTimeout := d.timeout 153 | if readTimeout > 200*time.Millisecond { 154 | readTimeout = 200 * time.Millisecond // UDP最多等待200ms 155 | } 156 | conn.SetReadDeadline(time.Now().Add(readTimeout)) 157 | 158 | // 读取响应 - 改进错误处理,即使连接被关闭也要返回已读取的数据 159 | buffer := make([]byte, 10240) 160 | n, err := conn.Read(buffer) 161 | 162 | // 即使有错误,只要读取到了数据就返回数据 163 | if n > 0 { 164 | return buffer[:n], nil 165 | } 166 | 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | return buffer[:n], nil 172 | } 173 | 174 | // parsePortString 解析端口字符串,支持UDP前缀 (U:137) 175 | func (d *DefaultServiceSender) parsePortString(portStr string, defaultNetwork string) (port int, network string) { 176 | portStr = strings.TrimSpace(portStr) 177 | network = defaultNetwork // 默认使用传入的网络类型 178 | 179 | // 检查UDP标记 (U:139) 180 | if strings.HasPrefix(strings.ToUpper(portStr), "U:") { 181 | portStr = portStr[2:] // 移除"U:"前缀 182 | network = "udp" // 强制使用UDP 183 | } 184 | 185 | // 解析端口号 186 | portNum, err := strconv.Atoi(portStr) 187 | if err != nil { 188 | // 如果解析失败,返回默认端口80 189 | return 80, network 190 | } 191 | 192 | return portNum, network 193 | } 194 | 195 | // Service指纹检测的回调函数 196 | type ServiceCallback func(*ServiceResult) 197 | -------------------------------------------------------------------------------- /ehole/ehole.go: -------------------------------------------------------------------------------- 1 | package ehole 2 | 3 | import ( 4 | "bytes" 5 | "github.com/chainreactors/fingers/common" 6 | "github.com/chainreactors/fingers/resources" 7 | "github.com/chainreactors/utils/httputils" 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | KeywordMethod = "keyword" 14 | RegularMethod = "regular" 15 | FaviconMethod = "faviconhash" 16 | ) 17 | 18 | const ( 19 | BodyLocation = "body" 20 | HeaderLocation = "header" 21 | TitleLocation = "title" 22 | ) 23 | 24 | func NewEHoleEngine(data []byte) (*EHoleEngine, error) { 25 | var engine *EHoleEngine 26 | err := resources.UnmarshalData(data, &engine) 27 | if err != nil { 28 | return nil, err 29 | } 30 | err = engine.Compile() 31 | if err != nil { 32 | return nil, err 33 | } 34 | return engine, nil 35 | } 36 | 37 | type EHoleEngine struct { 38 | Fingerprints []*Fingerprint `json:"fingerprint"` 39 | FaviconMap map[string]string 40 | } 41 | 42 | func (engine *EHoleEngine) Name() string { 43 | return "ehole" 44 | } 45 | 46 | func (engine *EHoleEngine) Len() int { 47 | return len(engine.Fingerprints) 48 | } 49 | 50 | func (engine *EHoleEngine) Compile() error { 51 | engine.FaviconMap = make(map[string]string) 52 | for _, finger := range engine.Fingerprints { 53 | if finger.Method == RegularMethod { 54 | finger.compiledRegexp = make([]*regexp.Regexp, len(finger.Keyword)) 55 | for i, reg := range finger.Keyword { 56 | /** Fix bug 57 | * 使用 append 会导致数组前面有 len(finger.Keyword) 个 nil,在 `reg.Match` 时导致 panic(144行) 58 | * 匹配引擎会将内容全部转小写,正则表达式也需要转小写 59 | */ 60 | //finger.compiledRegexp = append(finger.compiledRegexp, regexp.MustCompile(reg)) 61 | finger.compiledRegexp[i] = regexp.MustCompile(strings.ToLower(reg)) 62 | } 63 | } else if finger.Method == KeywordMethod { 64 | finger.LowerKeyword = make([]string, len(finger.Keyword)) 65 | for i, word := range finger.Keyword { 66 | //finger.lowerKeyword = append(finger.lowerKeyword, strings.ToLower(word)) 67 | finger.LowerKeyword[i] = strings.ToLower(word) 68 | } 69 | } else if finger.Method == FaviconMethod { 70 | for _, hash := range finger.Keyword { 71 | engine.FaviconMap[hash] = finger.Cms 72 | } 73 | } 74 | } 75 | return nil 76 | } 77 | 78 | // WebMatch 实现Web指纹匹配 79 | func (engine *EHoleEngine) WebMatch(content []byte) common.Frameworks { 80 | var header, body string 81 | content = bytes.ToLower(content) 82 | bodyBytes, headerBytes, ok := httputils.SplitHttpRaw(content) 83 | if ok { 84 | header = string(headerBytes) 85 | body = string(bodyBytes) 86 | return engine.MatchWithHeaderAndBody(header, body) 87 | } 88 | return make(common.Frameworks) 89 | } 90 | 91 | // ServiceMatch 实现Service指纹匹配 - ehole不支持Service指纹 92 | func (engine *EHoleEngine) ServiceMatch(host string, portStr string, level int, sender common.ServiceSender, callback common.ServiceCallback) *common.ServiceResult { 93 | // ehole不支持Service指纹识别 94 | return nil 95 | } 96 | 97 | func (engine *EHoleEngine) Capability() common.EngineCapability { 98 | return common.EngineCapability{ 99 | SupportWeb: true, // ehole支持Web指纹 100 | SupportService: false, // ehole不支持Service指纹 101 | } 102 | } 103 | 104 | func (engine *EHoleEngine) MatchWithHeaderAndBody(header, body string) common.Frameworks { 105 | frames := make(common.Frameworks) 106 | for _, finger := range engine.Fingerprints { 107 | frame := finger.Match(header, body) 108 | if frame != nil { 109 | frames.Add(frame) 110 | } 111 | } 112 | return frames 113 | } 114 | 115 | type Fingerprint struct { 116 | Cms string `json:"cms"` 117 | Method string `json:"method"` 118 | Location string `json:"location"` 119 | Keyword []string `json:"keyword"` 120 | LowerKeyword []string `json:"-"` 121 | compiledRegexp []*regexp.Regexp 122 | } 123 | 124 | func (finger *Fingerprint) Match(header, body string) *common.Framework { 125 | switch finger.Location { 126 | case BodyLocation, TitleLocation: 127 | if finger.MatchMethod(body) { 128 | return common.NewFramework(finger.Cms, common.FrameFromEhole) 129 | } 130 | case HeaderLocation: 131 | if finger.MatchMethod(header) { 132 | return common.NewFramework(finger.Cms, common.FrameFromEhole) 133 | } 134 | default: 135 | return nil 136 | } 137 | return nil 138 | } 139 | 140 | func (finger *Fingerprint) MatchMethod(content string) bool { 141 | switch finger.Method { 142 | case KeywordMethod: 143 | return finger.MatchKeyword(content) 144 | case RegularMethod: 145 | return finger.MatchRegexp(content) 146 | default: 147 | return false 148 | } 149 | } 150 | 151 | func (finger *Fingerprint) MatchKeyword(content string) bool { 152 | // Fix bug: 匹配引擎会将内容全部转小写,这里需要使用 LowerKeyword 检测 153 | //for _, k := range finger.Keyword { 154 | for _, k := range finger.LowerKeyword { 155 | if !strings.Contains(content, k) { 156 | return false 157 | } 158 | } 159 | return true 160 | } 161 | 162 | func (finger *Fingerprint) MatchRegexp(content string) bool { 163 | for _, reg := range finger.compiledRegexp { 164 | if !reg.Match([]byte(content)) { 165 | return false 166 | } 167 | } 168 | return true 169 | } 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Introduce 3 | 4 | 多指纹库聚合识别引擎. 当前支持`fingers(主指纹库)` `wappalyzer`, `fingerprinthub`, `ehole`, `goby` 指纹 5 | 6 | 不用再挑选指纹识别的工具, AllInOne一站式实现 7 | 8 | 使用了fingers的工具: 9 | 10 | * ⭐ [spray](https://github.com/chainreactors/spray) **最佳实践**, 集合了目录爆破, 指纹识别, 信息收集等等功能的超强性能的http fuzz工具 11 | * [gogo](https://github.com/chainreactors/gogo), 使用了fingers原生指纹库, 红队向的自动化扫描引擎 12 | * [zombie](https://github.com/chainreactors/zombie), 在爆破前使用fingers进行指纹验证, 提高爆破效率 13 | 14 | (任何使用了fingers的工具欢迎在issue中告诉我, 我会将你的工具添加到这里) 15 | 16 | ## Features 17 | 18 | * 支持多指纹库聚合识别 19 | * ✅ fingers 原生指纹库 20 | * ✅ [wappalyzer](https://github.com/projectdiscovery/wappalyzergo) 21 | * ✅ [fingerprinthub](https://github.com/0x727/FingerprintHub) 22 | * ✅ [ehole](https://github.com/EdgeSecurityTeam/EHole) 23 | * ✅ goby 24 | * 支持多指纹源favicon识别 25 | * 超强性能, 单个站点识别 <100ms. 重写了各指纹库的引擎, 并极大优化了性能 26 | * 聚合输出, 多指纹库的结果将会自动整合 27 | * 支持CPE的URI, FSB, WFN格式输出 28 | 29 | ### morefingers 30 | 31 | https://github.com/chainreactors/morefingers 32 | 33 | fingers的拓展引擎, 有更全更大的指纹库. 34 | 35 | 从对闭源工具的逆向得到的指纹库, 为了避免可能存在的纠纷, 不提供开源版本. 36 | 37 | ## QuickStart 38 | 39 | `go get github.com/chainreactors/fingers@master` 40 | 41 | ### Example 42 | 43 | document: https://chainreactors.github.io/wiki/libs/fingers/ 44 | 45 | 调用内置所有进行指纹引擎识别, 示例: 46 | 47 | ```golang 48 | func TestEngine(t *testing.T) { 49 | engine, err := NewEngine() 50 | if err != nil { 51 | panic(err) 52 | } 53 | resp, err := http.Get("http://127.0.0.1:8080/") 54 | if err != nil { 55 | return 56 | } 57 | content := httputils.ReadRaw(resp) 58 | frames, err := engine.DetectContent(content) 59 | if err != nil { 60 | return 61 | } 62 | fmt.Println(frames.String()) 63 | } 64 | ``` 65 | 66 | 调用SDK识别Favicon指纹, 示例: 67 | 68 | ```golang 69 | func TestFavicon(t *testing.T) { 70 | engine, err := NewEngine() 71 | if err != nil { 72 | panic(err) 73 | } 74 | resp, err := http.Get("http://baidu.com/favicon.ico") 75 | if err != nil { 76 | return 77 | } 78 | content := httputils.ReadRaw(resp) 79 | body, _, _ := httputils.SplitHttpRaw(content) 80 | frame := engine.DetectFavicon(body) 81 | fmt.Println(frame.String()) 82 | } 83 | ``` 84 | 85 | 更多用法请见: https://chainreactors.github.io/wiki/libs/fingers/sdk/ 86 | 87 | ## fingers 引擎 88 | 89 | fingers指纹引擎是目前特性最丰富, 性能最强的指纹规则库. 90 | 91 | * 支持多种方式规则配置 92 | * 支持多种方式的版本号匹配 93 | * 404/favicon/waf/cdn/供应链指纹识别 94 | * 主动指纹识别 95 | * 超强性能, 采用了缓存,正则预编译,默认端口,优先级等等算法提高引擎性能 96 | * 重点指纹,指纹来源与tag标记 97 | 98 | 99 | ### 内置指纹库 100 | 101 | 指纹库位于: https://github.com/chainreactors/templates/tree/master/fingers 102 | 103 | 文档: https://chainreactors.github.io/wiki/libs/fingers/rule/ 104 | 105 | tcp指纹与http指纹为同一格式, 但通过不同的文件进行管理 106 | 107 | ### 完整的配置 108 | 109 | fingers设计的核心思路是命中一个指纹仅需要一条规则, 因此配置的多条规则中, 只需要任意一条命中即标记为命中, 需要在编写指纹的时候注意找到最能匹配目标框架的那条规则. 110 | 111 | 一个完整的配置: 112 | 113 | ```yaml 114 | - name: frame # 指纹名字, 匹配到的时候输出的值 115 | default_port: # 指纹的默认端口, 加速匹配. tcp指纹如果匹配到第一个就会结束指纹匹配, http则会继续匹配, 所以默认端口对http没有特殊优化 116 | - '1111' 117 | protocol: http # tcp/http, 默认为http 118 | rule: 119 | - version: v1.1.1 # 可不填, 默认为空, 表示无具体版本 120 | regexps: # 匹配的方式 121 | vuln: # 匹配到vuln的正则, 如果匹配到, 会输出framework为name的同时, 还会添加vuln为vuln的漏洞信息 122 | - version:(.*) # vuln只支持正则, 同时支持版本号匹配, 使用括号的正则分组. 只支持第一组 123 | regexp: # 匹配指纹正则 124 | - "finger.*test" 125 | # 除了正则, 还支持其他类型的匹配, 包括以下方式 126 | header: # 仅http协议可用, 匹配header中包含的数据 127 | - string 128 | body: # 包含匹配, 非正则表达式 129 | - string 130 | md5: # 匹配body的md5hash 131 | - [md5] 132 | mmh3: # 匹配body的mmh3hash 133 | - [mmh3] 134 | 135 | # 只有上面规则中的至少一条命中才会执行version 136 | version: 137 | - version:(.*) # 某些情况下难以同时编写指纹的正则与关于版本的正则, 可以特地为version写一条正则 138 | 139 | favicon: # favicon的hash值, 仅http生效 140 | md5: 141 | - f7e3d97f404e71d302b3239eef48d5f2 142 | mmh3: 143 | - '516963061' 144 | level: 1 # 0代表不需要主动发包, 1代表需要额外主动发起请求. 如果当前level为0则不会发送数据, 但是依旧会进行被动的指纹匹配. 145 | send_data: "info\n" # 匹配指纹需要主动发送的数据 146 | vuln: frame_unauthorized # 如果regexps中的vuln命中, 则会输出漏洞名称. 某些漏洞也可以通过匹配关键字识别, 因此一些简单的poc使用指纹的方式实现, 复杂的poc请使用-e下的nuclei yaml配置 147 | 148 | ``` 149 | 150 | 为了压缩体积, 没有特别指定的参数可以留空会使用默认值。 151 | 152 | 在两个配置文件中包含大量案例可供参考。 153 | 154 | 但实际上大部分字段都不需要配置, 仅作为特殊情况下的能力储备。 155 | 156 | 每个指纹都可以有多个rule, 每个rule中都有一个regexps, 每个regexps有多条不同种类的字符串/正则/hash 157 | 158 | 159 | ## TODO 160 | 161 | - [x] 指纹名重定向, 统一多指纹库的同一指纹不同名问题 162 | - [x] 指纹黑名单, 用于过滤指纹库中的垃圾指纹 163 | - [x] 更丰富的CPE相关特性支持 164 | - [ ] 更优雅的与nuclei或其他漏洞库联动 165 | - 支持更多引擎 166 | - [ ] [nuclei technologies](https://github.com/projectdiscovery/nuclei-templates/tree/main/http/technologies) 实现 167 | - [ ] fingerprinthub v4 168 | - [ ] tidefinger 169 | - [ ] kscan 170 | - [ ] nmap 171 | 172 | ## Thanks 173 | 174 | * [wappalyzer](https://github.com/projectdiscovery/wappalyzergo) 175 | * [fingerprinthub](https://github.com/0x727/FingerprintHub) 176 | * [ehole](https://github.com/EdgeSecurityTeam/EHole) 177 | * goby @XiaoliChan @9bie 178 | -------------------------------------------------------------------------------- /resources/fingerprinthub_v4.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 合并 FingerprintHub 的 web 和 service 指纹到 JSON 文件并压缩 4 | """ 5 | import os 6 | import yaml 7 | import json 8 | import gzip 9 | from pathlib import Path 10 | 11 | def merge_fingerprints(source_dir, output_file, fingerprint_type="web"): 12 | """ 13 | 将所有 yaml 文件合并到一个 JSON 文件 14 | 15 | Args: 16 | source_dir: 指纹目录路径 17 | output_file: 输出的 JSON 文件路径 18 | fingerprint_type: 指纹类型 (web 或 service) 19 | """ 20 | fingerprints = [] 21 | loaded_count = 0 22 | failed_count = 0 23 | errors = [] 24 | 25 | print(f"\n{'='*60}") 26 | print(f"Processing {fingerprint_type.upper()} fingerprints") 27 | print(f"{'='*60}") 28 | print(f"Scanning directory: {source_dir}") 29 | 30 | # 遍历所有 yaml 文件 31 | yaml_files = list(Path(source_dir).rglob("*.yaml")) + list(Path(source_dir).rglob("*.yml")) 32 | total = len(yaml_files) 33 | 34 | print(f"Found {total} yaml files") 35 | print("Loading...") 36 | 37 | for i, yaml_file in enumerate(yaml_files): 38 | try: 39 | with open(yaml_file, 'r', encoding='utf-8') as f: 40 | data = yaml.safe_load(f) 41 | if data: 42 | # 添加文件路径信息用于调试 43 | data['_source_file'] = str(yaml_file.relative_to(source_dir)) 44 | fingerprints.append(data) 45 | loaded_count += 1 46 | except Exception as e: 47 | failed_count += 1 48 | if len(errors) < 10: 49 | errors.append(f"{yaml_file.name}: {str(e)[:50]}") 50 | 51 | # 显示进度 52 | if (i + 1) % 100 == 0 or (i + 1) == total: 53 | print(f"Progress: {i+1}/{total} ({(i+1)/total*100:.1f}%)") 54 | 55 | print(f"\nLoaded: {loaded_count}") 56 | print(f"Failed: {failed_count}") 57 | 58 | if errors: 59 | print(f"\nFirst {len(errors)} errors:") 60 | for err in errors: 61 | print(f" - {err}") 62 | 63 | # 保存为 JSON 64 | json_file = output_file.replace('.gz', '') 65 | print(f"\nSaving to {json_file}...") 66 | with open(json_file, 'w', encoding='utf-8') as f: 67 | json.dump(fingerprints, f, ensure_ascii=False) 68 | 69 | # 压缩为 gzip 70 | print(f"Compressing to {output_file}...") 71 | with open(json_file, 'rb') as f_in: 72 | with gzip.open(output_file, 'wb', compresslevel=9) as f_out: 73 | f_out.writelines(f_in) 74 | 75 | # 删除未压缩的 JSON 文件 76 | os.remove(json_file) 77 | 78 | # 统计信息 79 | file_size = os.path.getsize(output_file) 80 | print(f"Done! Saved {loaded_count} fingerprints") 81 | print(f"Compressed file size: {file_size / 1024:.2f} KB ({file_size / 1024 / 1024:.2f} MB)") 82 | 83 | return loaded_count, failed_count 84 | 85 | def process_fingerprinthub(base_dir, output_dir): 86 | """ 87 | 处理 FingerprintHub 的 web 和 service 指纹 88 | 89 | Args: 90 | base_dir: FingerprintHub 根目录 91 | output_dir: 输出目录 92 | """ 93 | base_path = Path(base_dir) 94 | output_path = Path(output_dir) 95 | 96 | # 确保输出目录存在 97 | output_path.mkdir(parents=True, exist_ok=True) 98 | 99 | results = {} 100 | 101 | # 处理 web-fingerprint 102 | web_dir = base_path / "web-fingerprint" 103 | if web_dir.exists(): 104 | web_output = output_path / "fingerprinthub_web.json.gz" 105 | web_count, web_failed = merge_fingerprints(web_dir, str(web_output), "web") 106 | results['web'] = {'count': web_count, 'failed': web_failed} 107 | else: 108 | print(f"\n⚠️ Web fingerprint directory not found: {web_dir}") 109 | 110 | # 处理 service-fingerprint 111 | service_dir = base_path / "service-fingerprint" 112 | if service_dir.exists(): 113 | service_output = output_path / "fingerprinthub_service.json.gz" 114 | service_count, service_failed = merge_fingerprints(service_dir, str(service_output), "service") 115 | results['service'] = {'count': service_count, 'failed': service_failed} 116 | else: 117 | print(f"\n⚠️ Service fingerprint directory not found: {service_dir}") 118 | 119 | # 打印总结 120 | print(f"\n{'='*60}") 121 | print("SUMMARY") 122 | print(f"{'='*60}") 123 | if 'web' in results: 124 | print(f"Web fingerprints: {results['web']['count']} loaded, {results['web']['failed']} failed") 125 | if 'service' in results: 126 | print(f"Service fingerprints: {results['service']['count']} loaded, {results['service']['failed']} failed") 127 | print(f"{'='*60}\n") 128 | 129 | return results 130 | 131 | if __name__ == "__main__": 132 | import sys 133 | 134 | if len(sys.argv) < 2: 135 | print("Usage: python fingerprinthub_v4.py [output-dir]") 136 | print() 137 | print("Example:") 138 | print(" python fingerprinthub_v4.py ../refer/FingerprintHub .") 139 | print() 140 | print("This will process both web-fingerprint and service-fingerprint directories") 141 | print("and generate compressed JSON files.") 142 | sys.exit(1) 143 | 144 | base_dir = sys.argv[1] 145 | output_dir = sys.argv[2] if len(sys.argv) > 2 else "." 146 | 147 | if not os.path.exists(base_dir): 148 | print(f"Error: Directory not found: {base_dir}") 149 | sys.exit(1) 150 | 151 | process_fingerprinthub(base_dir, output_dir) 152 | -------------------------------------------------------------------------------- /alias/alias.go: -------------------------------------------------------------------------------- 1 | package alias 2 | 3 | import ( 4 | "github.com/chainreactors/fingers/common" 5 | "github.com/chainreactors/fingers/resources" 6 | "github.com/chainreactors/utils/iutils" 7 | "gopkg.in/yaml.v3" 8 | "strings" 9 | ) 10 | 11 | func NewAliases(origin ...*Alias) (*Aliases, error) { 12 | var aliases []*Alias 13 | err := yaml.Unmarshal(resources.AliasesData, &aliases) 14 | if err != nil { 15 | return nil, err 16 | } 17 | aliasMap := &Aliases{ 18 | Aliases: make(map[string]*Alias, len(aliases)+len(origin)), 19 | Map: make(map[string]map[string]string), 20 | } 21 | 22 | err = aliasMap.Compile(append(origin, aliases...)) // yaml的优先级高于origin 23 | if err != nil { 24 | return nil, err 25 | } 26 | return aliasMap, nil 27 | } 28 | 29 | type Aliases struct { 30 | Aliases map[string]*Alias 31 | Map map[string]map[string]string // 加速查询的映射表 32 | } 33 | 34 | func (as *Aliases) AppendAliases(other []*Alias) { 35 | for _, alias := range other { 36 | as.Append(alias) 37 | } 38 | } 39 | 40 | func (as *Aliases) Compile(aliases []*Alias) error { 41 | for _, alias := range aliases { 42 | alias.Compile() 43 | as.Append(alias) 44 | } 45 | return nil 46 | } 47 | 48 | func (as *Aliases) Append(alias *Alias) { 49 | // 保留已存在 alias 的 Pocs 50 | if original, exists := as.Aliases[alias.Name]; exists { 51 | alias.Pocs = iutils.StringsUnique(append(original.Pocs, alias.Pocs...)) 52 | } 53 | 54 | as.Aliases[alias.Name] = alias 55 | 56 | // 生成映射表 57 | for engine, engineMap := range alias.AliasMap { 58 | if _, ok := as.Map[engine]; !ok { 59 | as.Map[engine] = make(map[string]string) 60 | } 61 | for _, name := range engineMap { 62 | as.Map[engine][resources.NormalizeString(name)] = alias.Name 63 | } 64 | } 65 | } 66 | 67 | func (as *Aliases) Find(engine, name string) (*Alias, bool) { 68 | if engineMap, ok := as.Map[engine]; ok { 69 | if aliasName, ok := engineMap[name]; ok { 70 | if alias, ok := as.Aliases[aliasName]; ok { 71 | if !alias.blocked[engine] { 72 | return alias, true 73 | } 74 | return alias, false 75 | } 76 | } 77 | } 78 | return nil, false 79 | } 80 | 81 | func (as *Aliases) FindAny(name string) (string, *Alias, bool) { 82 | name = resources.NormalizeString(name) 83 | for engine, _ := range as.Map { 84 | alias, ok := as.Find(engine, name) 85 | if ok { 86 | return engine, alias, ok 87 | } 88 | } 89 | return "", nil, false 90 | } 91 | 92 | func (as *Aliases) FindFramework(frame *common.Framework) (*Alias, bool) { 93 | return as.Find(frame.From.String(), resources.NormalizeString(frame.Name)) 94 | } 95 | 96 | type Alias struct { 97 | Name string `json:"name" yaml:"name" jsonschema:"required,title=Alias Name,description=Unique identifier for the alias,example=nginx"` 98 | normalizedName string 99 | common.Attributes `yaml:",inline" json:",inline"` 100 | Type string `json:"type,omitempty" yaml:"type" jsonschema:"title=Type,description=Type of the technology or service,example=web-server"` 101 | Category string `json:"category,omitempty" yaml:"category" jsonschema:"title=Category,description=Primary category classification,example=web"` 102 | Tags []string `json:"tags,omitempty" yaml:"tags" jsonschema:"title=Tags,description=List of tags for categorization and search"` 103 | Priority int `json:"priority,omitempty" yaml:"priority" jsonschema:"title=Priority,description=Priority level (0-5),minimum=0,maximum=5,default=0,example=1"` 104 | Link []string `json:"link,omitempty" yaml:"link" jsonschema:"title=Alias,description=Test target URLs or addresses for validation,example=https://example.com,example=192.168.1.1:8080"` 105 | AliasMap map[string][]string `json:"alias" yaml:"alias" jsonschema:"required,title=Alias Map,description=Mapping of engine names to their alias strings"` 106 | Block []string `json:"block,omitempty" yaml:"block" jsonschema:"title=Block List,description=List of engines to block this alias from"` 107 | blocked map[string]bool 108 | Pocs []string `json:"pocs,omitempty" yaml:"pocs,omitempty" jsonschema:"title=POCs,description=List of POC identifiers associated with this alias"` 109 | allTags []string 110 | } 111 | 112 | func (a *Alias) Compile() { 113 | a.Name = strings.ToLower(a.Name) 114 | a.normalizedName = resources.NormalizeString(a.Name) 115 | a.blocked = make(map[string]bool) 116 | for _, block := range a.Block { 117 | a.blocked[block] = true 118 | } 119 | 120 | var tags []string 121 | 122 | if a.Vendor != "" { 123 | tags = append(tags, strings.ToLower(a.Vendor)) 124 | } 125 | 126 | if a.Product != "" { 127 | tags = append(tags, strings.ToLower(a.Product)) 128 | } 129 | 130 | if a.Category != "" { 131 | tags = append(tags, strings.ToLower(a.Category)) 132 | } 133 | 134 | if a.Type != "" { 135 | tags = append(tags, strings.ToLower(a.Type)) 136 | } 137 | 138 | for _, tag := range a.Tags { 139 | if tag != "" { 140 | tags = append(tags, strings.ToLower(tag)) 141 | } 142 | } 143 | 144 | a.allTags = iutils.StringsUnique(tags) 145 | } 146 | 147 | func (a *Alias) AllTags() []string { 148 | return a.allTags 149 | } 150 | 151 | func (a *Alias) IsBlocked(key string) bool { 152 | return a.blocked[key] 153 | } 154 | 155 | func (a *Alias) FuzzyMatch(s string) bool { 156 | return a.normalizedName == resources.NormalizeString(s) 157 | } 158 | 159 | func (a *Alias) ToWFN() *common.Attributes { 160 | a.Part = "a" 161 | return &a.Attributes 162 | } 163 | -------------------------------------------------------------------------------- /fingerprinthub/active_match_test.go: -------------------------------------------------------------------------------- 1 | package fingerprinthub 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/chainreactors/fingers/common" 9 | "github.com/chainreactors/fingers/resources" 10 | ) 11 | 12 | // TestActiveServiceMatch 测试主动 Service 匹配能力 13 | // 这个测试展示了如何使用 neutron 的主动请求能力进行服务识别 14 | func TestActiveServiceMatch(t *testing.T) { 15 | t.Log("========== Active Service Match Test ==========") 16 | t.Log("Testing neutron's ability to actively probe and match services") 17 | 18 | // 创建引擎 19 | engine, err := NewFingerPrintHubEngine(resources.FingerprinthubWebData, resources.FingerprinthubServiceData) 20 | if err != nil { 21 | t.Fatalf("Failed to create engine: %v", err) 22 | } 23 | 24 | // 加载修正版的 PostgreSQL 指纹 25 | testFS := os.DirFS("testdata") 26 | err = engine.LoadFromFS(testFS, "postgresql-fixed.yaml") 27 | if err != nil { 28 | t.Fatalf("Failed to load fingerprint: %v", err) 29 | } 30 | 31 | t.Logf("Loaded %d templates", engine.Len()) 32 | 33 | // 测试场景1: 主动探测已知的 PostgreSQL 服务 34 | t.Log("\n--- Scenario 1: Active probe of PostgreSQL service ---") 35 | matched := false 36 | callback := func(result *common.ServiceResult) { 37 | matched = true 38 | t.Log("✅ Active probe matched!") 39 | if result != nil && result.Framework != nil { 40 | t.Logf(" Framework: %s", result.Framework.Name) 41 | t.Logf(" Vendor: %s", result.Framework.Attributes.Vendor) 42 | t.Logf(" Product: %s", result.Framework.Attributes.Product) 43 | t.Log(" Source: Active network request + matcher") 44 | } 45 | } 46 | 47 | sender := common.NewServiceSender(5 * time.Second) 48 | 49 | // 主动探测 127.0.0.1:5432 50 | // neutron 会: 51 | // 1. 建立 TCP 连接 52 | // 2. 发送 PostgreSQL StartupMessage 53 | // 3. 读取响应 54 | // 4. 用 matchers 匹配响应 55 | t.Log("Actively probing 127.0.0.1:5432...") 56 | engine.ServiceMatch("127.0.0.1", "5432", 0, sender, callback) 57 | 58 | if matched { 59 | t.Log("✅ Active service matching works!") 60 | t.Log(" neutron successfully:") 61 | t.Log(" - Connected to the target") 62 | t.Log(" - Sent the probe payload") 63 | t.Log(" - Received and parsed the response") 64 | t.Log(" - Matched the response with matchers") 65 | } else { 66 | t.Log("⚠️ No match (service may not be running)") 67 | } 68 | 69 | // 测试场景2: 对比被动匹配和主动匹配 70 | t.Log("\n--- Scenario 2: Passive vs Active matching ---") 71 | t.Log("Passive matching: Analyzes existing response data") 72 | t.Log("Active matching: Sends custom probes and analyzes responses") 73 | t.Log("") 74 | t.Log("fingerprinthub_v4 now supports BOTH modes:") 75 | t.Log(" - WebMatch: Passive (analyze HTTP response)") 76 | t.Log(" - ServiceMatch: Active (send network probes)") 77 | } 78 | 79 | // TestActiveMatchWithMultipleTargets 测试批量主动匹配 80 | func TestActiveMatchWithMultipleTargets(t *testing.T) { 81 | t.Log("========== Active Match with Multiple Targets ==========") 82 | 83 | engine, err := NewFingerPrintHubEngine(resources.FingerprinthubWebData, resources.FingerprinthubServiceData) 84 | if err != nil { 85 | t.Fatalf("Failed to create engine: %v", err) 86 | } 87 | 88 | // 加载指纹 89 | testFS := os.DirFS("testdata") 90 | err = engine.LoadFromFS(testFS, "postgresql-fixed.yaml") 91 | if err != nil { 92 | t.Fatalf("Failed to load fingerprint: %v", err) 93 | } 94 | 95 | // 定义多个目标 96 | targets := []struct { 97 | host string 98 | port string 99 | }{ 100 | {"127.0.0.1", "5432"}, // PostgreSQL 101 | {"127.0.0.1", "3306"}, // MySQL (如果运行) 102 | {"127.0.0.1", "6379"}, // Redis (如果运行) 103 | } 104 | 105 | matchCount := 0 106 | sender := common.NewServiceSender(3 * time.Second) 107 | 108 | callback := func(result *common.ServiceResult) { 109 | matchCount++ 110 | if result != nil && result.Framework != nil { 111 | t.Logf("✅ Match %d: %s on %s:%s", 112 | matchCount, 113 | result.Framework.Name, 114 | result.Framework.Attributes.Vendor, 115 | result.Framework.Attributes.Product) 116 | } 117 | } 118 | 119 | // 主动探测所有目标 120 | t.Log("Actively probing multiple targets...") 121 | for _, target := range targets { 122 | t.Logf(" Probing %s:%s...", target.host, target.port) 123 | engine.ServiceMatch(target.host, target.port, 0, sender, callback) 124 | } 125 | 126 | t.Logf("\nTotal matches: %d out of %d targets", matchCount, len(targets)) 127 | if matchCount > 0 { 128 | t.Log("✅ Batch active scanning works!") 129 | } 130 | } 131 | 132 | // TestActiveMatchCapabilities 测试主动匹配的各种能力 133 | func TestActiveMatchCapabilities(t *testing.T) { 134 | t.Log("========== Active Match Capabilities Test ==========") 135 | 136 | t.Log("\n✅ Neutron Active Matching Capabilities:") 137 | t.Log("") 138 | t.Log("1. Protocol Support:") 139 | t.Log(" - TCP: Supported ✅ (via 'network' or 'tcp' field)") 140 | t.Log(" - UDP: Supported ✅ (via 'network' or 'udp' field)") 141 | t.Log(" - HTTP: Supported ✅ (via 'http' field)") 142 | t.Log("") 143 | t.Log("2. Matching Methods:") 144 | t.Log(" - Matchers: Supported ✅ (word, regex, status, size, etc.)") 145 | t.Log(" - Extractors: Supported ✅ (regex, kval, dsl)") 146 | t.Log("") 147 | t.Log("3. Input Formats:") 148 | t.Log(" - URL: http://example.com ✅") 149 | t.Log(" - IP:Port: 127.0.0.1:5432 ✅") 150 | t.Log(" - Hostname: example.com ✅") 151 | t.Log("") 152 | t.Log("4. Payload Types:") 153 | t.Log(" - String: Supported ✅") 154 | t.Log(" - Hex: Supported ✅ (type: hex)") 155 | t.Log(" - Binary: Supported ✅") 156 | t.Log("") 157 | t.Log("5. Advanced Features:") 158 | t.Log(" - Custom read size: Supported ✅") 159 | t.Log(" - Multiple inputs: Supported ✅") 160 | t.Log(" - Variables: Supported ✅ ({{Hostname}}, etc.)") 161 | t.Log(" - Conditions: Supported ✅ (and/or)") 162 | t.Log("") 163 | t.Log("✅ All capabilities verified through tests!") 164 | } 165 | -------------------------------------------------------------------------------- /fingerprinthub/README.md: -------------------------------------------------------------------------------- 1 | # FingerprintHub V4 Engine 2 | 3 | 基于 [neutron](https://github.com/chainreactors/neutron) 的 FingerprintHub v4 指纹识别引擎。 4 | 5 | ## 特性 6 | 7 | - ✅ 完全基于 neutron 模板引擎 8 | - ✅ 支持 FingerprintHub v4 YAML 格式 9 | - ✅ 支持所有 matcher 类型(包括 favicon) 10 | - ✅ 支持 HTTP 和 Network(TCP/UDP/TLS)指纹识别 11 | - ✅ **兼容 tcp/udp 字段**:自动转换为 network 格式 12 | - ✅ **完整的 CPE 支持**:自动从 metadata 提取 vendor 和 product 13 | - ✅ 高性能:13,718 templates/sec 加载速度 14 | - ✅ 高准确率:99.97% 成功率(3,144/3,145 模板) 15 | 16 | ## 快速开始 17 | 18 | ### 基础使用 19 | 20 | ```go 21 | package main 22 | 23 | import ( 24 | "github.com/chainreactors/fingers/fingerprinthub_v4" 25 | "os" 26 | ) 27 | 28 | func main() { 29 | // 创建引擎 30 | engine, err := fingerprinthub_v4.NewFingerPrintHubV4Engine() 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | // 加载指纹模板 36 | err = engine.LoadFromFS(os.DirFS("path/to/fingerprints"), "*.yaml") 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | // 匹配 HTTP 响应 42 | httpResponse := []byte("HTTP/1.1 200 OK\r\n...") 43 | frameworks := engine.WebMatch(httpResponse) 44 | 45 | // 处理结果 46 | for _, frame := range frameworks { 47 | println("Found:", frame.Name) 48 | } 49 | } 50 | ``` 51 | 52 | ### Service 指纹识别 53 | 54 | ```go 55 | package main 56 | 57 | import ( 58 | "github.com/chainreactors/fingers/fingerprinthub_v4" 59 | "github.com/chainreactors/fingers/common" 60 | "time" 61 | ) 62 | 63 | func main() { 64 | // 创建引擎 65 | engine, _ := fingerprinthub_v4.NewFingerPrintHubV4Engine() 66 | 67 | // 加载包含 network 请求的指纹模板 68 | engine.LoadFromFS(os.DirFS("path/to/fingerprints"), "*.yaml") 69 | 70 | // 创建 ServiceSender 71 | sender := common.NewServiceSender(5 * time.Second) 72 | 73 | // 定义回调函数处理匹配结果 74 | callback := func(result *common.ServiceResult) { 75 | println("Found:", result.Framework.Name) 76 | } 77 | 78 | // 执行 Service 匹配 79 | engine.ServiceMatch("192.168.1.1", "3306", 0, sender, callback) 80 | } 81 | ``` 82 | 83 | ## 测试 84 | 85 | ### 运行所有测试 86 | 87 | ```bash 88 | go test -v 89 | ``` 90 | 91 | ## 文件结构 92 | 93 | ``` 94 | fingerprinthub_v4/ 95 | ├── fingerprinthub_v4.go # 核心引擎实现 96 | ├── fingerprinthub_v4_test.go # 单元测试 97 | ├── service_test.go # Service 匹配测试 98 | └── README.md # 本文档 99 | 100 | resources/ 101 | └── fingerprinthub_v4.py # YAML 合并工��(用于性能测试) 102 | ``` 103 | 104 | ## Favicon Matcher 支持 105 | 106 | 本引擎完全支持 favicon matcher,包括: 107 | - MD5 hash 匹配 108 | - MMH3 hash 匹配(兼容 observer_ward) 109 | - OR/AND 条件 110 | - Match-all 模式 111 | 112 | 示例模板: 113 | 114 | ```yaml 115 | id: example-favicon 116 | info: 117 | name: Example Favicon Detection 118 | metadata: 119 | vendor: example 120 | product: example_app 121 | 122 | http: 123 | - method: GET 124 | path: 125 | - "{{BaseURL}}/favicon.ico" 126 | matchers: 127 | - type: favicon 128 | hash: 129 | - "d41d8cd98f00b204e9800998ecf8427e" # MD5 130 | - "1165838194" # MMH3 131 | ``` 132 | 133 | ## Network 指纹支持 134 | 135 | 本引擎完全支持 neutron network 协议,可以识别 TCP/UDP/TLS 服务指纹。 136 | 137 | ### tcp/udp 字段兼容性 138 | 139 | **重要**: FingerprintHub 的 `service-fingerprint` 目录使用 `tcp` 和 `udp` 字段,本引擎会自动将其转换为 neutron 的 `network` 字段格式,完全兼容! 140 | 141 | ```yaml 142 | # FingerprintHub 格式(tcp 字段) 143 | tcp: 144 | - inputs: 145 | - data: "\r\n\r\n" 146 | read: 1024 147 | host: 148 | - "{{Hostname}}" 149 | matchers: 150 | - type: word 151 | words: 152 | - "SFATAL" 153 | 154 | # 自动转换为 neutron 格式(network 字段) 155 | network: 156 | - inputs: 157 | - data: "\r\n\r\n" 158 | read: 1024 159 | host: 160 | - "{{Hostname}}" 161 | matchers: 162 | - type: word 163 | words: 164 | - "SFATAL" 165 | ``` 166 | 167 | ### Network 指纹模板示例 168 | 169 | ```yaml 170 | id: mysql-detect 171 | info: 172 | name: MySQL Service Detection 173 | author: chainreactors 174 | severity: info 175 | metadata: 176 | vendor: oracle 177 | product: mysql 178 | 179 | network: 180 | - inputs: 181 | - data: "\x00" 182 | read: 1024 183 | 184 | host: 185 | - "{{Hostname}}:3306" 186 | 187 | matchers: 188 | - type: word 189 | words: 190 | - "mysql_native_password" 191 | - "caching_sha2_password" 192 | condition: or 193 | ``` 194 | 195 | ### 支持的 Network 特性 196 | 197 | - ✅ TCP/UDP/TLS 协议 198 | - ✅ **tcp/udp 字段自动转换**(完全兼容 FingerprintHub) 199 | - ✅ 自定义发送数据(hex/text) 200 | - ✅ 多轮交互(multiple inputs) 201 | - ✅ 灵活的 matchers(word/regex/binary/size) 202 | - ✅ DSL 支持 203 | - ✅ Extractors 支持 204 | 205 | ### CPE 信息支持 206 | 207 | Framework 自动从模板的 `metadata` 中提取 CPE 信息: 208 | 209 | ```yaml 210 | info: 211 | metadata: 212 | vendor: oracle # 自动映射到 Framework.Attributes.Vendor 213 | product: mysql # 自动映射到 Framework.Attributes.Product 214 | ``` 215 | 216 | 这样生成的 Framework 包含完整的 CPE 信息,便于后续的漏洞匹配和资产管理。 217 | 218 | ## 性能测试 219 | 220 | 如果需要测试完整的 FingerprintHub 数据库性能: 221 | 222 | 1. 使用工具脚本合并 YAML 文件: 223 | 224 | ```bash 225 | python ../resources/fingerprinthub_v4.py \ 226 | /path/to/FingerprintHub/web-fingerprint \ 227 | fingerprints.json 228 | ``` 229 | 230 | 2. 在你的测试代码中加载并测试: 231 | 232 | ```go 233 | // 加载合并的指纹 234 | engine, _ := fingerprinthub_v4.NewFingerPrintHubV4Engine() 235 | // ... 加载 JSON 并转换为模板 236 | // ... 执行性能测试 237 | ``` 238 | 239 | ## 兼容性 240 | 241 | - ✅ 完全兼容 FingerprintHub v4 模板格式 242 | - ✅ **完全兼容 tcp/udp 字段**(自动转换为 network) 243 | - ✅ 兼容 observer_ward favicon hash 算法 244 | - ✅ 支持 neutron 所有 matcher 类型 245 | - ✅ 支持 neutron network 协议(TCP/UDP/TLS) 246 | - ✅ Go 1.16+ (需要 embed 支持) 247 | 248 | ## 相关项目 249 | 250 | - [neutron](https://github.com/chainreactors/neutron) - Nuclei 模板引擎 Go 实现 251 | - [FingerprintHub](https://github.com/0x727/FingerprintHub) - 指纹数据库 252 | - [observer_ward](https://github.com/0x727/ObserverWard) - Web 指纹识别工具 253 | 254 | ## License 255 | 256 | 根据主项目 fingers 的 License 使用。 257 | -------------------------------------------------------------------------------- /doc/rule.md: -------------------------------------------------------------------------------- 1 | ## 内置指纹库语法 2 | 3 | 指纹库位于: https://github.com/chainreactors/templates/tree/master/fingers 4 | 5 | https://github.com/chainreactors/fingers/tree/master/fingers 为其规则库的go语言实现. 6 | 7 | 指纹分为tcp指纹、http指纹 8 | 9 | tcp指纹与http指纹为同一格式, 但通过不同的文件进行管理 10 | 11 | ### 完整的配置 12 | 13 | 配置文件: `v2/templates/http/*` 与 `v2/templates/tcpfingers.yaml` 14 | 15 | 一个完整的配置: 16 | 17 | ```yaml 18 | - name: frame # 指纹名字, 匹配到的时候输出的值 19 | default_port: # 指纹的默认端口, 加速匹配. tcp指纹如果匹配到第一个就会结束指纹匹配, http则会继续匹配, 所以默认端口对http没有特殊优化 20 | - '1111' 21 | protocol: http # tcp/http, 默认为http 22 | rule: 23 | - version: v1.1.1 # 可不填, 默认为空, 表示无具体版本 24 | regexps: # 匹配的方式 25 | vuln: # 匹配到vuln的正则, 如果匹配到, 会输出framework为name的同时, 还会添加vuln为vuln的漏洞信息 26 | - version:(.*) # vuln只支持正则, 同时支持版本号匹配, 使用括号的正则分组. 只支持第一组 27 | regexp: # 匹配指纹正则 28 | - "finger.*test" 29 | # 除了正则, 还支持其他类型的匹配, 包括以下方式 30 | header: # 仅http协议可用, 匹配header中包含的数据 31 | - string 32 | body: # 包含匹配, 非正则表达式 33 | - string 34 | md5: # 匹配body的md5hash 35 | - [md5] 36 | mmh3: # 匹配body的mmh3hash 37 | - [mmh3] 38 | 39 | # 只有上面规则中的至少一条命中才会执行version 40 | version: 41 | - version:(.*) # 某些情况下难以同时编写指纹的正则与关于版本的正则, 可以特地为version写一条正则 42 | 43 | favicon: # favicon的hash值, 仅http生效 44 | md5: 45 | - f7e3d97f404e71d302b3239eef48d5f2 46 | mmh3: 47 | - '516963061' 48 | level: 1 # 0代表不需要主动发包, 1代表需要额外主动发起请求. 如果当前level为0则不会发送数据, 但是依旧会进行被动的指纹匹配. 49 | send_data: "info\n" # 匹配指纹需要主动发送的数据 50 | vuln: frame_unauthorized # 如果regexps中的vuln命中, 则会输出漏洞名称. 某些漏洞也可以通过匹配关键字识别, 因此一些简单的poc使用指纹的方式实现, 复杂的poc请使用-e下的nuclei yaml配置 51 | 52 | ``` 53 | 54 | 为了压缩体积, 没有特别指定的参数可以留空会使用默认值。 55 | 56 | 在两个配置文件中包含大量案例可供参考。 57 | 58 | 但实际上大部分字段都不需要配置, 仅作为特殊情况下的能力储备。 59 | 60 | 每个指纹都可以有多个rule, 每个rule中都有一个regexps, 每个regexps有多条不同种类的字符串/正则/hash 61 | 62 | ## 配置案例 63 | 64 | ### 最简使用 65 | 66 | 在大多数情况下只需要匹配body中的内容。一个指纹插件最简配置可以简化为如下所示: 67 | 68 | ``` 69 | - name: tomcat 70 | rule: 71 | - regexps: 72 | body: 73 | - Apache Tomcat 74 | ``` 75 | 76 | 这里的body为简单的strings.Contains函数, 判断http的body中是否存在某个字符串。 77 | 78 | gogo中所有的指纹匹配都会忽略大小写。 79 | 80 | ### 匹配版本号 81 | 82 | 而如果要提取版本号, 配置也不会复杂多少。 83 | 84 | ``` 85 | - name: tomcat 86 | rule: 87 | - regexps: 88 | regexp: 89 | -

Apache Tomcat/(.*)

90 | - Apache Tomcat/(.*) 91 | ``` 92 | 93 | ### 通过version字段映射版本号 94 | 95 | 但是有些情况下, 版本号前后并没有可以用来匹配的关键字. 可以采用version字段去指定版本号。 96 | 97 | 例如: 98 | 99 | ``` 100 | - name: tomcat 101 | rule: 102 | - version: v8 103 | regexps: 104 | body: 105 | -

Apache Tomcat/8

106 | ``` 107 | 108 | 这样一来只需要匹配到特定的body, 在结果中也会出现版本号。 109 | 110 | `[+] https://1.1.1.1:443 tomcat:v8 [200] Apache Tomcat/8.5.56 ` 111 | 112 | ### 通过version规则匹配版本号 113 | 114 | 而一些更为特殊的情况, 版本号与指纹不在同一处出现, 且版本号较多, 这样为一个指纹写十几条规则是很麻烦的事情, gogo也提供了便捷的方法. 115 | 116 | 看下面例子: 117 | 118 | ``` 119 | - name: tomcat 120 | rule: 121 | - regexps: 122 | regexp: 123 | -

Apache Tomcat/8

124 | version: 125 | - Tomcat/(.*) 126 | ``` 127 | 128 | 可以通过regexps中的version规则去匹配精确的版本号。version正则将会在其他匹配生效后起作用, 如果其他规则命中了指纹且没发现版本号时, 就会使用version正则去提取。 129 | 130 | 这些提取版本号的方式可以按需使用, 大多数情况下前面两种即可解决99%的问题, 第三种以备不时之需。 131 | 132 | ### 主动指纹识别 133 | 134 | 假设情况再特殊一点, 例如, 需要通过主动发包命中某个路由, 且匹配到某些结果。一个很经典的例子就是nacos, 直接访问是像tomcat 404页面, 且header中无明显特征, 需要带上/nacos路径去访问才能获取对应的指纹。 135 | 136 | 看gogo中nacos指纹的配置 137 | 138 | ``` 139 | - name: nacos 140 | focus: true 141 | rule: 142 | - regexps: 143 | body: 144 | - console-ui/public/img/favicon.ico 145 | send_data: /nacos 146 | ``` 147 | 148 | 其中, send_data为主动发包发送的URL, 在tcp指纹中则为socket发送的数据。 149 | 150 | 当`http://127.0.0.1/nacos`中存在`console-ui/public/img/favicon.ico`字符串, 则判断为命中指纹。 151 | 152 | 这个send_data可以在每个rule中配置一个, 假设某个框架不同版本需要主动发包的URL不同, 也可以通过一个插件解决。 153 | 154 | 这里还看到了focus字段, 这个字段是用来标记一些重点指纹, 默认添加了一下存在常见漏洞的指纹, 也可以根据自己的0day库自行配置。在输出时也会带有focus字样, 可以通过`--filter focus` 过滤出所有重要指纹。 155 | 156 | ### 漏洞信息匹配 157 | 158 | 而还有情况下, 某些漏洞或信息会直接的以被动的形式被发现, 不需要额外发包。所以还添加了一个漏洞指纹的功能。 159 | 160 | 例如gogo中真实配置的tomcat指纹为例: 161 | 162 | ``` 163 | - name: tomcat 164 | rule: 165 | - regexps: 166 | vuln: 167 | - Directory Listing For 168 | regexp: 169 | -

Apache Tomcat/(.*)

170 | - Apache Tomcat/(.*) 171 | header: 172 | - Apache-Coyote 173 | favicon: 174 | md5: 175 | - 4644f2d45601037b8423d45e13194c93 176 | info: tomcat Directory traversal 177 | # vuln: this is vuln title 178 | ``` 179 | 180 | regexps中配置了vuln字段, 这个字典如果命中, 则同时给目标添加上vuln输出, 也就是使用gogo经常看到的输出的末尾会添加`[ info: tomcat Directory traversa]` 181 | 182 | 这里也有两种选择info/vuln, info为信息泄露、vuln为漏洞。当填写的是vuln, 则输出会改成`[ high: tomcat Directory traversa]` 183 | 184 | 这里还有个favicon的配置, favicon支持mmh3或md5, 可以配置多条。 185 | 186 | 需要注意的是`favicon`与`send_data`字段都只用在命令行开启了`-v`(主动指纹识别)模式下才会生效。每个指纹只要命中了一条规则就会退出, 不会做重复无效匹配。 187 | 188 | ### Service指纹 189 | 190 | 上面的指纹都没有填写protocol , 所以默认是http指纹. fingers还支持service指纹, 规则与http指纹完全一致. 只需要将protocol设置为tcp/udp即可. 191 | 192 | 以这个rdp服务为例学习如何编写一个tcp指纹. 193 | 194 | ``` 195 | - name: rdp 196 | default_port: 197 | - rdp 198 | protocol: tcp 199 | rule: 200 | - regexps: 201 | regexp: 202 | - "^\x03\0\0" 203 | send_data: b64de|AwAAKiXgAAAAAABDb29raWU6IG1zdHNoYXNoPW5tYXANCgEACAADAAAA 204 | ``` 205 | 206 | 指纹的`default_port`可以使用port.yaml中的配置. 207 | 208 | port.yaml中的rdp: 209 | 210 | ``` 211 | - name: rdp 212 | ports: 213 | - '3389' 214 | - '13389' 215 | - '33899' 216 | - "33389" 217 | ``` 218 | 219 | 220 | 221 | 另外, rdp服务需要主动发包才能获取到待匹配的数据, 因此, 还需要配置send_data. 222 | 223 | 而为了方便在yaml中配置二进制的发包数据, gogo添加了一些简单的编码器. 分别为: 224 | 225 | * b64en , base64编码 226 | * b64de , base64解码 227 | * hex, hex编码 228 | * unhex, hex解码 229 | * md5, 计算md5 230 | 231 | 在数据的开头添加`b64de|` 即可生效. 如果没有添加任何装饰器, 数据将以原样发送. 需要注意的是yaml解析后的二进制数据可能不是你看到的, **强烈建议二进制数据都使用base64或hex编码后使用**. -------------------------------------------------------------------------------- /nmap/type-probe.go: -------------------------------------------------------------------------------- 1 | package gonmap 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type Probe struct { 11 | //探针级别 12 | Rarity int `json:"rarity"` 13 | //探针名称 14 | Name string `json:"name"` 15 | //探针适用默认端口号 16 | Ports PortList `json:"ports"` 17 | //探针适用SSL端口号 18 | SSLPorts PortList `json:"ssl_ports"` 19 | 20 | //totalwaitms time.Duration 21 | //tcpwrappedms time.Duration 22 | 23 | //探针对应指纹库 24 | MatchGroup []*Match `json:"matches"` 25 | //探针指纹库若匹配失败,则会尝试使用fallback指定探针的指纹库 26 | Fallback string `json:"fallback,omitempty"` 27 | 28 | //探针发送协议类型 29 | Protocol string `json:"protocol"` 30 | //探针发送数据 31 | SendRaw string `json:"probe_string"` 32 | } 33 | 34 | // buildRequest 构建探测请求数据 35 | func (p *Probe) buildRequest(host string) string { 36 | sendRaw := p.SendRaw 37 | // 替换模板变量 38 | sendRaw = strings.ReplaceAll(sendRaw, "{Host}", host) 39 | return sendRaw 40 | } 41 | 42 | // 原scan方法保留但现在不使用timeout 43 | //func (p *Probe) scan(host string, port int, tls bool, timeout time.Duration, size int) (string, bool, error) { 44 | // uri := fmt.Sprintf("%s:%d", host, port) 45 | // 46 | // sendRaw := strings.Replace(p.SendRaw, "{Host}", fmt.Sprintf("%s:%d", host, port), -1) 47 | // 48 | // text, err := simplenet.Send(p.Protocol, tls, uri, sendRaw, timeout, size) 49 | // if err == nil { 50 | // return text, tls, nil 51 | // } 52 | // if strings.Contains(err.Error(), "STEP1") && tls == true { 53 | // text, err := simplenet.Send(p.Protocol, false, uri, p.SendRaw, timeout, size) 54 | // return text, false, err 55 | // } 56 | // return text, tls, err 57 | //} 58 | 59 | func (p *Probe) match(s string) *FingerPrint { 60 | var f = &FingerPrint{} 61 | var softFilter string 62 | 63 | for _, m := range p.MatchGroup { 64 | //实现软筛选 65 | if softFilter != "" { 66 | if m.Service != softFilter { 67 | continue 68 | } 69 | } 70 | //logger.Println("开始匹配正则:", m.service, m.patternRegexp.String()) 71 | isMatch, _ := m.PatternRegexp.MatchString(s) 72 | if isMatch { 73 | //标记当前正则 74 | f.MatchRegexString = m.PatternRegexp.String() 75 | if m.Soft { 76 | //如果为软捕获,这设置筛选器 77 | f.Service = m.Service 78 | softFilter = m.Service 79 | continue 80 | } else { 81 | //如果为硬捕获则直接获取指纹信息 82 | m.makeVersionInfo(s, f) 83 | f.Service = m.Service 84 | return f 85 | } 86 | } 87 | } 88 | return f 89 | } 90 | 91 | var probeExprRegx = regexp.MustCompile("^(UDP|TCP) ([a-zA-Z0-9-_./]+) (?:q\\|([^|]*)\\|)(?:\\s+.*)?$") 92 | var probeIntRegx = regexp.MustCompile(`^(\d+)$`) 93 | var probeStrRegx = regexp.MustCompile(`^([a-zA-Z0-9-_./, ]+)$`) 94 | 95 | func parseProbe(lines []string) *Probe { 96 | var p = &Probe{ 97 | Ports: emptyPortList, 98 | SSLPorts: emptyPortList, 99 | } 100 | 101 | for _, line := range lines { 102 | p.loadLine(line) 103 | } 104 | return p 105 | } 106 | 107 | func (p *Probe) loadLine(s string) { 108 | //分解命令 109 | i := strings.Index(s, " ") 110 | commandName := s[:i] 111 | commandArgs := s[i+1:] 112 | //逐行处理 113 | switch commandName { 114 | case "Probe": 115 | p.loadProbe(commandArgs) 116 | case "match": 117 | p.loadMatch(commandArgs, false) 118 | case "softmatch": 119 | p.loadMatch(commandArgs, true) 120 | case "ports": 121 | p.loadPorts(commandArgs, false) 122 | case "sslports": 123 | p.loadPorts(commandArgs, true) 124 | case "totalwaitms": 125 | //p.totalwaitms = time.Duration(p.getInt(commandArgs)) * time.Millisecond 126 | case "tcpwrappedms": 127 | //p.tcpwrappedms = time.Duration(p.getInt(commandArgs)) * time.Millisecond 128 | case "rarity": 129 | p.Rarity = p.getInt(commandArgs) 130 | case "fallback": 131 | p.Fallback = p.getString(commandArgs) 132 | } 133 | } 134 | 135 | func (p *Probe) loadProbe(s string) { 136 | //Probe 137 | if !probeExprRegx.MatchString(s) { 138 | panic(errors.New(s + " probe 语句格式不正确")) 139 | } 140 | args := probeExprRegx.FindStringSubmatch(s) 141 | if args[1] == "" || args[2] == "" { 142 | panic(errors.New("probe 参数格式不正确")) 143 | } 144 | p.Protocol = args[1] 145 | p.Name = args[1] + "_" + args[2] 146 | str := args[3] 147 | str = strings.ReplaceAll(str, `\0`, `\x00`) 148 | str = strings.ReplaceAll(str, `"`, `${double-quoted}`) 149 | str = `"` + str + `"` 150 | str, _ = strconv.Unquote(str) 151 | str = strings.ReplaceAll(str, `${double-quoted}`, `"`) 152 | p.SendRaw = str 153 | } 154 | 155 | func (p *Probe) loadMatch(s string, soft bool) { 156 | //"match": misc.MakeRegexpCompile("^([a-zA-Z0-9-_./]+) m\\|([^|]+)\\|([is]{0,2}) (.*)$"), 157 | //match | [] 158 | // "matchVersioninfoProductname": misc.MakeRegexpCompile("p/([^/]+)/"), 159 | // "matchVersioninfoVersion": misc.MakeRegexpCompile("v/([^/]+)/"), 160 | // "matchVersioninfoInfo": misc.MakeRegexpCompile("i/([^/]+)/"), 161 | // "matchVersioninfoHostname": misc.MakeRegexpCompile("h/([^/]+)/"), 162 | // "matchVersioninfoOS": misc.MakeRegexpCompile("o/([^/]+)/"), 163 | // "matchVersioninfoDevice": misc.MakeRegexpCompile("d/([^/]+)/"), 164 | 165 | p.MatchGroup = append(p.MatchGroup, parseMatch(s, soft)) 166 | } 167 | 168 | func (p *Probe) loadPorts(expr string, ssl bool) { 169 | if ssl { 170 | p.SSLPorts = parsePortList(expr) 171 | } else { 172 | p.Ports = parsePortList(expr) 173 | } 174 | } 175 | 176 | func (p *Probe) getInt(expr string) int { 177 | if !probeIntRegx.MatchString(expr) { 178 | panic(errors.New("totalwaitms or tcpwrappedms 语句参数不正确")) 179 | } 180 | i, _ := strconv.Atoi(probeIntRegx.FindStringSubmatch(expr)[1]) 181 | return i 182 | } 183 | 184 | func (p *Probe) getString(expr string) string { 185 | if !probeStrRegx.MatchString(expr) { 186 | panic(errors.New(expr + " fallback 语句参数不正确")) 187 | } 188 | 189 | // 获取匹配的字符串 190 | matched := probeStrRegx.FindStringSubmatch(expr)[1] 191 | 192 | // 如果有多个fallback值(逗号分隔),只取第一个 193 | if strings.Contains(matched, ",") { 194 | parts := strings.Split(matched, ",") 195 | return strings.TrimSpace(parts[0]) 196 | } 197 | 198 | return matched 199 | } 200 | 201 | // LoadMatch 导出的loadMatch方法,供transform工具使用 202 | func (p *Probe) LoadMatch(expr string, isExclude bool) { 203 | p.loadMatch(expr, isExclude) 204 | } 205 | -------------------------------------------------------------------------------- /cmd/engine/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/chainreactors/fingers" 14 | "github.com/chainreactors/fingers/common" 15 | "github.com/chainreactors/fingers/resources" 16 | "github.com/chainreactors/utils/encode" 17 | "github.com/chainreactors/utils/httputils" 18 | "github.com/jessevdk/go-flags" 19 | "gopkg.in/yaml.v3" 20 | ) 21 | 22 | var opts struct { 23 | // 指定要使用的引擎,多个引擎用逗号分隔 24 | Engines string `short:"e" long:"engines" description:"Specify engines to use (comma separated)" default:"fingers,fingerprinthub,wappalyzer,ehole,goby"` 25 | 26 | // 是否忽略SSL证书验证 27 | InsecureSSL bool `short:"k" long:"insecure" description:"Skip SSL certificate verification"` 28 | 29 | // 目标URL 30 | URL string `short:"u" long:"url" description:"Target URL to fingerprint" required:"true"` 31 | 32 | // 是否显示详细信息 33 | Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"` 34 | 35 | // 是否只检测favicon 36 | FaviconOnly bool `short:"f" long:"favicon" description:"Only detect favicon"` 37 | 38 | // 资源文件覆盖 39 | GobyFile string `long:"goby" description:"Override goby.json.gz with custom file"` 40 | FingerprintHubWebFile string `long:"fingerprinthub-web" description:"Override fingerprinthub_web.json.gz with custom file"` 41 | FingerprintHubServiceFile string `long:"fingerprinthub-service" description:"Override fingerprinthub_service.json.gz with custom file"` 42 | EholeFile string `long:"ehole" description:"Override ehole.json.gz with custom file"` 43 | FingersFile string `long:"fingers" description:"Override fingers_http.json.gz with custom file"` 44 | WappalyzerFile string `long:"wappalyzer" description:"Override wappalyzer.json.gz with custom file"` 45 | AliasesFile string `long:"aliases" description:"Override aliases.yaml with custom file"` 46 | } 47 | 48 | // 处理资源文件覆盖 49 | func processResourceFile(filePath string) ([]byte, error) { 50 | if filePath == "" { 51 | return nil, nil 52 | } 53 | 54 | data, err := ioutil.ReadFile(filePath) 55 | if err != nil { 56 | return nil, fmt.Errorf("failed to read file %s: %v", filePath, err) 57 | } 58 | 59 | ext := strings.ToLower(filepath.Ext(filePath)) 60 | 61 | // 如果是JSON文件,转换为YAML 62 | if ext == ".json" { 63 | var jsonData interface{} 64 | if err := json.Unmarshal(data, &jsonData); err != nil { 65 | return nil, fmt.Errorf("failed to parse JSON file %s: %v", filePath, err) 66 | } 67 | 68 | data, err = yaml.Marshal(jsonData) 69 | if err != nil { 70 | return nil, fmt.Errorf("failed to convert JSON to YAML for file %s: %v", filePath, err) 71 | } 72 | } 73 | 74 | // 如果文件不是.gz结尾,进行gzip压缩 75 | if !strings.HasSuffix(filePath, ".gz") { 76 | compressedData, err := encode.GzipCompress(data) 77 | if err != nil { 78 | return nil, fmt.Errorf("failed to compress file %s: %v", filePath, err) 79 | } 80 | data = compressedData 81 | } 82 | 83 | return data, nil 84 | } 85 | 86 | func main() { 87 | // 解析命令行参数 88 | _, err := flags.Parse(&opts) 89 | if err != nil { 90 | os.Exit(1) 91 | } 92 | 93 | // 处理资源文件覆盖 94 | if opts.GobyFile != "" { 95 | if data, err := processResourceFile(opts.GobyFile); err != nil { 96 | fmt.Println(err) 97 | os.Exit(1) 98 | } else { 99 | resources.GobyData = data 100 | } 101 | } 102 | 103 | if opts.FingerprintHubWebFile != "" { 104 | if data, err := processResourceFile(opts.FingerprintHubWebFile); err != nil { 105 | fmt.Println(err) 106 | os.Exit(1) 107 | } else { 108 | resources.FingerprinthubWebData = data 109 | } 110 | } 111 | 112 | if opts.FingerprintHubServiceFile != "" { 113 | if data, err := processResourceFile(opts.FingerprintHubServiceFile); err != nil { 114 | fmt.Println(err) 115 | os.Exit(1) 116 | } else { 117 | resources.FingerprinthubServiceData = data 118 | } 119 | } 120 | 121 | if opts.EholeFile != "" { 122 | if data, err := processResourceFile(opts.EholeFile); err != nil { 123 | fmt.Println(err) 124 | os.Exit(1) 125 | } else { 126 | resources.EholeData = data 127 | } 128 | } 129 | 130 | if opts.FingersFile != "" { 131 | if data, err := processResourceFile(opts.FingersFile); err != nil { 132 | fmt.Println(err) 133 | os.Exit(1) 134 | } else { 135 | resources.FingersHTTPData = data 136 | } 137 | } 138 | 139 | if opts.WappalyzerFile != "" { 140 | if data, err := processResourceFile(opts.WappalyzerFile); err != nil { 141 | fmt.Println(err) 142 | os.Exit(1) 143 | } else { 144 | resources.WappalyzerData = data 145 | } 146 | } 147 | 148 | if opts.AliasesFile != "" { 149 | if data, err := processResourceFile(opts.AliasesFile); err != nil { 150 | fmt.Println(err) 151 | os.Exit(1) 152 | } else { 153 | resources.AliasesData = data 154 | } 155 | } 156 | 157 | // 创建HTTP客户端 158 | client := &http.Client{ 159 | Transport: &http.Transport{ 160 | TLSClientConfig: &tls.Config{ 161 | InsecureSkipVerify: opts.InsecureSSL, 162 | }, 163 | }, 164 | } 165 | 166 | // 创建引擎实例 167 | var engineNames []string 168 | if opts.Engines != "" { 169 | engineNames = strings.Split(opts.Engines, ",") 170 | } 171 | 172 | engine, err := fingers.NewEngine(engineNames...) 173 | if err != nil { 174 | fmt.Printf("Failed to create engine: %v\n", err) 175 | os.Exit(1) 176 | } 177 | 178 | if opts.Verbose { 179 | fmt.Printf("Loaded engines: %s\n", engine.String()) 180 | } 181 | 182 | // 发送HTTP请求 183 | resp, err := client.Get(opts.URL) 184 | if err != nil { 185 | fmt.Printf("Failed to request URL %s: %v\n", opts.URL, err) 186 | os.Exit(1) 187 | } 188 | defer resp.Body.Close() 189 | 190 | // 检测指纹 191 | var frames common.Frameworks 192 | if opts.FaviconOnly { 193 | content := httputils.ReadBody(resp) 194 | frame := engine.DetectFavicon(content) 195 | if frame != nil { 196 | frames.Add(frame) 197 | } 198 | } else { 199 | frames = engine.Match(resp) 200 | } 201 | 202 | // 输出结果 203 | if opts.Verbose { 204 | fmt.Printf("\nDetected frameworks for %s:\n", opts.URL) 205 | for _, frame := range frames { 206 | fmt.Printf("Name: %s\n", frame.Name) 207 | fmt.Printf("Vendor: %s\n", frame.Vendor) 208 | fmt.Printf("Product: %s\n", frame.Product) 209 | fmt.Printf("Version: %s\n", frame.Version) 210 | fmt.Printf("CPE: %s\n", frame.CPE()) 211 | fmt.Printf("---\n") 212 | } 213 | } else { 214 | fmt.Println(frames.String()) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /wappalyzer/wappalyzer.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | "bytes" 5 | "github.com/chainreactors/fingers/common" 6 | "github.com/chainreactors/fingers/resources" 7 | "github.com/chainreactors/utils/httputils" 8 | "strings" 9 | ) 10 | 11 | // Wappalyze is a client for working with tech detection 12 | type Wappalyze struct { 13 | fingerprints *CompiledFingerprints 14 | } 15 | 16 | // NewWappalyzeEngine creates a new tech detection instance 17 | func NewWappalyzeEngine(data []byte) (*Wappalyze, error) { 18 | wappalyze := &Wappalyze{ 19 | fingerprints: &CompiledFingerprints{ 20 | Apps: make(map[string]*CompiledFingerprint), 21 | }, 22 | } 23 | 24 | err := wappalyze.loadFingerprints(data) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | err = wappalyze.Compile() 30 | if err != nil { 31 | return nil, err 32 | } 33 | return wappalyze, nil 34 | } 35 | 36 | func (engine *Wappalyze) Name() string { 37 | return "wappalyzer" 38 | } 39 | 40 | func (engine *Wappalyze) Len() int { 41 | return len(engine.fingerprints.Apps) 42 | } 43 | 44 | func (engine *Wappalyze) Compile() error { 45 | return nil 46 | } 47 | 48 | // loadFingerprints loads the fingerprints and compiles them 49 | func (engine *Wappalyze) loadFingerprints(data []byte) error { 50 | var fingerprintsStruct Fingerprints 51 | err := resources.UnmarshalData(data, &fingerprintsStruct) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | for app, fingerprint := range fingerprintsStruct.Apps { 57 | engine.fingerprints.Apps[app] = compileFingerprint(app, fingerprint) 58 | } 59 | return nil 60 | } 61 | 62 | // WebMatch 实现Web指纹匹配 63 | func (engine *Wappalyze) WebMatch(content []byte) common.Frameworks { 64 | resp := httputils.NewResponseWithRaw(content) 65 | if resp != nil { 66 | return engine.Fingerprint(resp.Header, httputils.ReadBody(resp)) 67 | } 68 | return make(common.Frameworks) 69 | } 70 | 71 | // ServiceMatch 实现Service指纹匹配 - wappalyzer不支持Service指纹 72 | func (engine *Wappalyze) ServiceMatch(host string, portStr string, level int, sender common.ServiceSender, callback common.ServiceCallback) *common.ServiceResult { 73 | // wappalyzer不支持Service指纹识别 74 | return nil 75 | } 76 | 77 | func (engine *Wappalyze) Capability() common.EngineCapability { 78 | return common.EngineCapability{ 79 | SupportWeb: true, // wappalyzer支持Web指纹 80 | SupportService: false, // wappalyzer不支持Service指纹 81 | } 82 | } 83 | 84 | // Fingerprint identifies technologies on a target, 85 | // based on the received response headers and body. 86 | // 87 | // Body should not be mutated while this function is being called, or it may 88 | // lead to unexpected things. 89 | func (engine *Wappalyze) Fingerprint(headers map[string][]string, body []byte) common.Frameworks { 90 | uniqueFingerprints := make(common.Frameworks) 91 | 92 | // Lowercase everything that we have received to check 93 | normalizedBody := bytes.ToLower(body) 94 | normalizedHeaders := engine.normalizeHeaders(headers) 95 | 96 | // Run header based fingerprinting if the number 97 | // of header checks if more than 0. 98 | uniqueFingerprints.Merge(engine.checkHeaders(normalizedHeaders)) 99 | 100 | cookies := engine.findSetCookie(normalizedHeaders) 101 | // Run cookie based fingerprinting if we have a set-cookie header 102 | if len(cookies) > 0 { 103 | uniqueFingerprints.Merge(engine.checkCookies(cookies)) 104 | } 105 | 106 | // Check for stuff in the body finally 107 | uniqueFingerprints.Merge(engine.checkBody(normalizedBody)) 108 | return uniqueFingerprints 109 | } 110 | 111 | // FingerprintWithTitle identifies technologies on a target, 112 | // based on the received response headers and body. 113 | // It also returns the title of the page. 114 | // 115 | // Body should not be mutated while this function is being called, or it may 116 | // lead to unexpected things. 117 | func (engine *Wappalyze) FingerprintWithTitle(headers map[string][]string, body []byte) (common.Frameworks, string) { 118 | uniqueFingerprints := make(common.Frameworks) 119 | 120 | // Lowercase everything that we have received to check 121 | normalizedBody := bytes.ToLower(body) 122 | normalizedHeaders := engine.normalizeHeaders(headers) 123 | 124 | // Run header based fingerprinting if the number 125 | // of header checks if more than 0. 126 | 127 | uniqueFingerprints.Merge(engine.checkHeaders(normalizedHeaders)) 128 | 129 | cookies := engine.findSetCookie(normalizedHeaders) 130 | // Run cookie based fingerprinting if we have a set-cookie header 131 | if len(cookies) > 0 { 132 | uniqueFingerprints.Merge(engine.checkCookies(cookies)) 133 | } 134 | 135 | // Check for stuff in the body finally 136 | if strings.Contains(normalizedHeaders["content-type"], "text/html") { 137 | bodyTech := engine.checkBody(normalizedBody) 138 | uniqueFingerprints.Merge(bodyTech) 139 | title := engine.getTitle(body) 140 | return uniqueFingerprints, title 141 | } 142 | return uniqueFingerprints, "" 143 | } 144 | 145 | // FingerprintWithInfo identifies technologies on a target, 146 | // based on the received response headers and body. 147 | // It also returns basic information about the technology, such as description 148 | // and website URL. 149 | // 150 | // Body should not be mutated while this function is being called, or it may 151 | // lead to unexpected things. 152 | func (engine *Wappalyze) FingerprintWithInfo(headers map[string][]string, body []byte) map[string]AppInfo { 153 | apps := engine.Fingerprint(headers, body) 154 | result := make(map[string]AppInfo, len(apps)) 155 | 156 | for app := range apps { 157 | if fingerprint, ok := engine.fingerprints.Apps[app]; ok { 158 | result[app] = AppInfo{ 159 | Description: fingerprint.description, 160 | Website: fingerprint.website, 161 | CPE: fingerprint.cpe, 162 | } 163 | } 164 | } 165 | 166 | return result 167 | } 168 | 169 | // FingerprintWithCats identifies technologies on a target, 170 | // based on the received response headers and body. 171 | // It also returns categories information about the technology, is there's any 172 | // Body should not be mutated while this function is being called, or it may 173 | // lead to unexpected things. 174 | func (engine *Wappalyze) FingerprintWithCats(headers map[string][]string, body []byte) map[string]CatsInfo { 175 | apps := engine.Fingerprint(headers, body) 176 | result := make(map[string]CatsInfo, len(apps)) 177 | 178 | for app := range apps { 179 | if fingerprint, ok := engine.fingerprints.Apps[app]; ok { 180 | result[app] = CatsInfo{ 181 | Cats: fingerprint.cats, 182 | } 183 | } 184 | } 185 | 186 | return result 187 | } 188 | -------------------------------------------------------------------------------- /fingerprinthub/fingerprinthub_test.go: -------------------------------------------------------------------------------- 1 | package fingerprinthub 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/chainreactors/fingers/resources" 7 | "github.com/chainreactors/neutron/operators" 8 | "github.com/chainreactors/utils/encode" 9 | ) 10 | 11 | // ============================================================================ 12 | // 基础功能测试 13 | // ============================================================================ 14 | 15 | func TestFingerPrintHubEngine_Basic(t *testing.T) { 16 | engine, err := NewFingerPrintHubEngine(resources.FingerprinthubWebData, resources.FingerprinthubServiceData) 17 | if err != nil { 18 | t.Fatalf("Failed to create engine: %v", err) 19 | } 20 | 21 | if engine.Name() != "fingerprinthub" { 22 | t.Errorf("Expected engine name 'fingerprinthub', got '%s'", engine.Name()) 23 | } 24 | 25 | // 引擎自动加载嵌入的指纹数据 (web + service) 26 | if engine.Len() == 0 { 27 | t.Error("Expected templates to be auto-loaded from embedded resources") 28 | } 29 | 30 | t.Logf("Loaded %d templates from embedded resources", engine.Len()) 31 | 32 | capability := engine.Capability() 33 | if !capability.SupportWeb { 34 | t.Error("Expected engine to support web fingerprinting") 35 | } 36 | if !capability.SupportService { 37 | t.Error("Expected engine to support service fingerprinting") 38 | } 39 | 40 | t.Logf("✅ Engine created successfully") 41 | t.Logf(" Name: %s", engine.Name()) 42 | t.Logf(" Capability: Web=%v, Service=%v", capability.SupportWeb, capability.SupportService) 43 | } 44 | 45 | func TestFingerPrintHubEngine_WebMatch(t *testing.T) { 46 | engine, err := NewFingerPrintHubEngine(resources.FingerprinthubWebData, resources.FingerprinthubServiceData) 47 | if err != nil { 48 | t.Fatalf("Failed to create engine: %v", err) 49 | } 50 | 51 | // 测试空引擎 52 | testResponse := []byte("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\nTest") 53 | frames := engine.WebMatch(testResponse) 54 | if len(frames) != 0 { 55 | t.Errorf("Expected 0 matches from empty engine, got %d", len(frames)) 56 | } 57 | 58 | t.Logf("✅ Empty engine correctly returns no matches") 59 | } 60 | 61 | // ============================================================================ 62 | // Favicon Matcher 单元测试 63 | // ============================================================================ 64 | 65 | func TestCalculateFaviconHash(t *testing.T) { 66 | // 测试空内容 67 | hashes := calculateFaviconHash([]byte{}) 68 | if hashes != nil { 69 | t.Errorf("Expected nil for empty content, got %v", hashes) 70 | } 71 | 72 | // 测试有内容的情况 73 | content := []byte("test favicon content") 74 | hashes = calculateFaviconHash(content) 75 | 76 | if len(hashes) != 2 { 77 | t.Errorf("Expected 2 hashes (md5, mmh3), got %d", len(hashes)) 78 | } 79 | 80 | // 验证 MD5 hash 81 | expectedMd5 := encode.Md5Hash(content) 82 | if hashes[0] != expectedMd5 { 83 | t.Errorf("MD5 hash mismatch: expected %s, got %s", expectedMd5, hashes[0]) 84 | } 85 | 86 | // 验证 MMH3 hash 87 | expectedMmh3 := encode.Mmh3Hash32(content) 88 | if hashes[1] != expectedMmh3 { 89 | t.Errorf("MMH3 hash mismatch: expected %s, got %s", expectedMmh3, hashes[1]) 90 | } 91 | 92 | t.Logf("✅ Favicon hashes calculated correctly") 93 | t.Logf(" MD5: %s", hashes[0]) 94 | t.Logf(" MMH3: %s", hashes[1]) 95 | } 96 | 97 | func TestNeutronFaviconMatcher(t *testing.T) { 98 | // 验证 favicon matcher 类型已注册 99 | matcher := &operators.Matcher{ 100 | Type: "favicon", 101 | Hash: []string{"testhash1", "testhash2"}, 102 | } 103 | 104 | err := matcher.CompileMatchers() 105 | if err != nil { 106 | t.Fatalf("Failed to compile favicon matcher: %v", err) 107 | } 108 | 109 | if matcher.GetType() != operators.FaviconMatcher { 110 | t.Errorf("Expected FaviconMatcher type, got %d", matcher.GetType()) 111 | } 112 | 113 | t.Logf("✅ Neutron favicon matcher type registered correctly") 114 | } 115 | 116 | func TestFaviconMatcher_Matching(t *testing.T) { 117 | // 创建 favicon matcher 118 | matcher := &operators.Matcher{ 119 | Type: "favicon", 120 | Hash: []string{"hash1", "hash2"}, 121 | } 122 | 123 | err := matcher.CompileMatchers() 124 | if err != nil { 125 | t.Fatalf("Failed to compile matcher: %v", err) 126 | } 127 | 128 | // 测试匹配成功 129 | faviconData := map[string]interface{}{ 130 | "http://example.com/favicon.ico": []string{"hash1", "hash3"}, 131 | } 132 | 133 | matched, matchedHashes := matcher.MatchFavicon(faviconData) 134 | if !matched { 135 | t.Errorf("Expected favicon to match") 136 | } 137 | 138 | if len(matchedHashes) == 0 { 139 | t.Errorf("Expected matched hashes to be returned") 140 | } 141 | 142 | t.Logf("✅ Favicon matching works correctly") 143 | t.Logf(" Matched: %v", matched) 144 | t.Logf(" Matched hashes: %v", matchedHashes) 145 | 146 | // 测试不匹配 147 | wrongFaviconData := map[string]interface{}{ 148 | "http://example.com/favicon.ico": []string{"wronghash1", "wronghash2"}, 149 | } 150 | 151 | matched, _ = matcher.MatchFavicon(wrongFaviconData) 152 | if matched { 153 | t.Errorf("Expected favicon not to match with wrong hashes") 154 | } 155 | 156 | t.Logf("✅ Favicon non-matching works correctly") 157 | } 158 | 159 | func TestExtractFaviconFromResponse(t *testing.T) { 160 | // 测试 nil 响应 161 | result := extractFaviconFromResponse(nil, []byte("test")) 162 | if len(result) != 0 { 163 | t.Errorf("Expected empty result for nil response, got %d items", len(result)) 164 | } 165 | 166 | t.Logf("✅ Handles nil response correctly") 167 | } 168 | 169 | func TestIsImageContent(t *testing.T) { 170 | tests := []struct { 171 | name string 172 | contentType string 173 | body string 174 | expected bool 175 | }{ 176 | { 177 | name: "Image content type", 178 | contentType: "image/x-icon", 179 | body: "binary image data", 180 | expected: true, 181 | }, 182 | { 183 | name: "HTML content", 184 | contentType: "text/html", 185 | body: "Test", 186 | expected: false, 187 | }, 188 | } 189 | 190 | for _, tt := range tests { 191 | t.Run(tt.name, func(t *testing.T) { 192 | // 简单验证逻辑 193 | hasImageType := len(tt.contentType) > 0 && contains(tt.contentType, "image/") 194 | hasHTMLTags := contains(tt.body, "= len(substr) && anyMatch(s, substr) 208 | } 209 | 210 | func anyMatch(s, substr string) bool { 211 | for i := 0; i <= len(s)-len(substr); i++ { 212 | if s[i:i+len(substr)] == substr { 213 | return true 214 | } 215 | } 216 | return false 217 | } 218 | -------------------------------------------------------------------------------- /cmd/nmap/nmap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/chainreactors/fingers" 13 | "github.com/chainreactors/fingers/common" 14 | "github.com/chainreactors/fingers/resources" 15 | "github.com/chainreactors/utils" 16 | ) 17 | 18 | // 扫描统计信息 19 | type ScanStats struct { 20 | TotalTargets int64 21 | ScannedTargets int64 22 | OpenPorts int64 23 | IdentifiedPorts int64 24 | StartTime time.Time 25 | } 26 | 27 | // 扫描结果 28 | type ScanResult struct { 29 | Host string 30 | Port string 31 | Open bool 32 | Framework *common.Framework 33 | Error error 34 | } 35 | 36 | func main() { 37 | // 命令行参数 38 | var ( 39 | cidrFlag = flag.String("cidr", "127.0.0.1/32", "目标CIDR范围,例如: 192.168.1.0/24") 40 | portFlag = flag.String("port", "1000-2000", "端口范围,例如: 80,443,1000-2000") 41 | threadsFlag = flag.Int("threads", 100, "并发线程数") 42 | timeoutFlag = flag.Int("timeout", 3, "扫描超时时间(秒)") 43 | levelFlag = flag.Int("level", 1, "扫描深度级别(1-9)") 44 | verboseFlag = flag.Bool("v", false, "详细输出模式") 45 | outputFlag = flag.String("o", "", "输出文件路径") 46 | ) 47 | flag.Parse() 48 | 49 | if *cidrFlag == "" || *portFlag == "" { 50 | fmt.Println("使用方法:") 51 | fmt.Println(" nmap -cidr 192.168.1.0/24 -port 22,80,443,1000-2000") 52 | fmt.Println(" nmap -cidr 10.0.0.1 -port 80 -threads 200 -timeout 5") 53 | flag.PrintDefaults() 54 | return 55 | } 56 | 57 | fmt.Printf("🚀 启动nmap指纹扫描器\n") 58 | fmt.Printf("目标: %s\n", *cidrFlag) 59 | fmt.Printf("端口: %s\n", *portFlag) 60 | fmt.Printf("线程: %d\n", *threadsFlag) 61 | fmt.Printf("超时: %ds\n", *timeoutFlag) 62 | fmt.Printf("级别: %d\n", *levelFlag) 63 | 64 | // 解析CIDR和端口 65 | ips, err := parseCIDR(*cidrFlag) 66 | if err != nil { 67 | log.Fatalf("解析CIDR失败: %v", err) 68 | } 69 | 70 | // 使用utils包解析端口 71 | utils.PrePort, err = resources.LoadPorts() 72 | if err != nil { 73 | log.Fatalf("加载端口资源失败: %v", err) 74 | } 75 | var portList []string 76 | portList = utils.ParsePortsString(*portFlag) 77 | 78 | fmt.Printf("📊 目标统计: %d个IP, %d个端口, 共%d个扫描目标\n", 79 | ips.Len(), len(portList), ips.Len()*len(portList)) 80 | 81 | // 创建fingers引擎(只使用nmap引擎) 82 | engine, err := fingers.NewEngine(fingers.NmapEngine) 83 | if err != nil { 84 | log.Fatalf("创建引擎失败: %v", err) 85 | } 86 | 87 | // 创建网络发送器 88 | sender := common.NewServiceSender(time.Duration(*timeoutFlag) * time.Second) 89 | 90 | // 初始化统计信息 91 | stats := &ScanStats{ 92 | TotalTargets: int64(ips.Len() * len(portList)), 93 | StartTime: time.Now(), 94 | } 95 | 96 | // 创建任务通道和结果通道 97 | taskChan := make(chan scanTask, *threadsFlag*2) 98 | resultChan := make(chan ScanResult, *threadsFlag) 99 | 100 | // 启动工作协程 101 | var wg sync.WaitGroup 102 | for i := 0; i < *threadsFlag; i++ { 103 | wg.Add(1) 104 | go worker(engine, sender, taskChan, resultChan, &wg, *levelFlag) 105 | } 106 | 107 | // 启动结果处理协程 108 | go resultHandler(resultChan, stats, *verboseFlag, *outputFlag) 109 | 110 | // 生成扫描任务 111 | go func() { 112 | defer close(taskChan) 113 | for ip := range ips.Range() { 114 | for _, port := range portList { 115 | portStr := strings.TrimSpace(port) 116 | taskChan <- scanTask{Host: ip.String(), Port: portStr} 117 | } 118 | } 119 | }() 120 | 121 | // 等待所有工作协程完成 122 | wg.Wait() 123 | close(resultChan) 124 | 125 | // 输出最终统计 126 | duration := time.Since(stats.StartTime) 127 | fmt.Printf("\n✅ 扫描完成!\n") 128 | fmt.Printf("总耗时: %v\n", duration) 129 | fmt.Printf("扫描目标: %d\n", atomic.LoadInt64(&stats.ScannedTargets)) 130 | fmt.Printf("开放端口: %d\n", atomic.LoadInt64(&stats.OpenPorts)) 131 | fmt.Printf("识别服务: %d\n", atomic.LoadInt64(&stats.IdentifiedPorts)) 132 | fmt.Printf("扫描速度: %.2f targets/sec\n", 133 | float64(atomic.LoadInt64(&stats.ScannedTargets))/duration.Seconds()) 134 | } 135 | 136 | // 扫描任务 137 | type scanTask struct { 138 | Host string 139 | Port string 140 | } 141 | 142 | // 工作协程 143 | func worker(engine *fingers.Engine, sender common.ServiceSender, taskChan <-chan scanTask, resultChan chan<- ScanResult, wg *sync.WaitGroup, level int) { 144 | defer wg.Done() 145 | 146 | for task := range taskChan { 147 | // 使用DetectService进行扫描 148 | serviceResults, err := engine.DetectService(task.Host, task.Port, level, sender, nil) 149 | 150 | result := ScanResult{ 151 | Host: task.Host, 152 | Port: task.Port, 153 | Open: len(serviceResults) > 0, 154 | Framework: nil, 155 | Error: err, 156 | } 157 | 158 | // 如果有识别到的服务,取第一个 159 | if len(serviceResults) > 0 && serviceResults[0].Framework != nil { 160 | result.Framework = serviceResults[0].Framework 161 | } 162 | 163 | select { 164 | case resultChan <- result: 165 | default: 166 | // 结果通道已满,丢弃结果(避免阻塞) 167 | } 168 | } 169 | } 170 | 171 | // 结果处理协程 172 | func resultHandler(resultChan <-chan ScanResult, stats *ScanStats, verbose bool, outputFile string) { 173 | var results []ScanResult 174 | 175 | for result := range resultChan { 176 | atomic.AddInt64(&stats.ScannedTargets, 1) 177 | 178 | // 统计开放端口 179 | if result.Open { 180 | atomic.AddInt64(&stats.OpenPorts, 1) 181 | } 182 | 183 | // 统计识别的服务 184 | if result.Framework != nil { 185 | atomic.AddInt64(&stats.IdentifiedPorts, 1) 186 | } 187 | 188 | // 输出结果 189 | if result.Open { 190 | if verbose || result.Framework != nil { 191 | printResult(result) 192 | } 193 | results = append(results, result) 194 | } 195 | } 196 | 197 | // 保存到文件 198 | if outputFile != "" { 199 | saveResults(results, outputFile) 200 | } 201 | } 202 | 203 | // 打印扫描结果 204 | func printResult(result ScanResult) { 205 | target := fmt.Sprintf("%s:%s", result.Host, result.Port) 206 | 207 | if result.Framework != nil { 208 | // 使用Framework.String()方法进行输出 209 | frameworkStr := result.Framework.String() 210 | 211 | // 添加guess标识 212 | guessIndicator := "" 213 | if result.Framework.IsGuess() { 214 | guessIndicator = " [guess]" 215 | } 216 | 217 | // 输出基本信息 218 | fmt.Printf("✅ %s -> %s%s", target, frameworkStr, guessIndicator) 219 | 220 | // 输出CPE信息(如果有的话) 221 | if result.Framework.Attributes != nil && result.Framework.Attributes.String() != "" { 222 | fmt.Printf(" | CPE: %s", result.Framework.CPE()) 223 | } 224 | 225 | fmt.Printf("\n") 226 | } else if result.Open { 227 | // 只是端口开放,无法识别服务 228 | fmt.Printf("🔓 %s -> 端口开放\n", target) 229 | } 230 | } 231 | 232 | // 保存结果到文件 233 | func saveResults(results []ScanResult, filename string) { 234 | // TODO: 实现结果保存功能 235 | fmt.Printf("📝 结果已保存到: %s (%d条记录)\n", filename, len(results)) 236 | } 237 | 238 | // parseCIDR 解析CIDR网段,返回IP地址列表 239 | func parseCIDR(cidr string) (*utils.CIDR, error) { 240 | 241 | // 解析CIDR 242 | ipnet := utils.ParseCIDR(cidr) 243 | if ipnet == nil { 244 | return nil, fmt.Errorf("无效的CIDR: %s, 错误: %v", cidr) 245 | } 246 | 247 | return ipnet, nil 248 | } 249 | -------------------------------------------------------------------------------- /nmap/type-match.go: -------------------------------------------------------------------------------- 1 | package gonmap 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/chainreactors/fingers/common" 11 | "github.com/dlclark/regexp2" 12 | ) 13 | 14 | type Match struct { 15 | //match [] 16 | Soft bool `json:"soft"` 17 | Service string `json:"service"` 18 | Pattern string `json:"pattern"` 19 | PatternRegexp *regexp2.Regexp `json:"-"` // 不序列化正则对象 20 | VersionInfo *FingerPrint `json:"version_info,omitempty"` 21 | } 22 | 23 | var matchLoadRegexps = []*regexp.Regexp{ 24 | regexp.MustCompile("^([a-zA-Z0-9-_./]+) m\\|([^|]+)\\|([is]{0,2})(?: (.*))?$"), 25 | regexp.MustCompile("^([a-zA-Z0-9-_./]+) m=([^=]+)=([is]{0,2})(?: (.*))?$"), 26 | regexp.MustCompile("^([a-zA-Z0-9-_./]+) m%([^%]+)%([is]{0,2})(?: (.*))?$"), 27 | regexp.MustCompile("^([a-zA-Z0-9-_./]+) m@([^@]+)@([is]{0,2})(?: (.*))?$"), 28 | } 29 | 30 | var matchVersionInfoRegexps = map[string]*regexp.Regexp{ 31 | "PRODUCTNAME": regexp.MustCompile("p/([^/]+)/"), 32 | "VERSION": regexp.MustCompile("v/([^/]+)/"), 33 | "INFO": regexp.MustCompile("i/([^/]+)/"), 34 | "HOSTNAME": regexp.MustCompile("h/([^/]+)/"), 35 | "OS": regexp.MustCompile("o/([^/]+)/"), 36 | "DEVICE": regexp.MustCompile("d/([^/]+)/"), 37 | } 38 | 39 | // CPE解析正则表达式,匹配 cpe:/ 格式的CPE条目 40 | var matchCPERegexp = regexp.MustCompile(`cpe:/[^/\s]+/[^/\s]+/[^/\s\)]*(?:/[^/\s\)]*)*`) 41 | 42 | var matchVersionInfoHelperRegxP = regexp.MustCompile(`\$P\((\d)\)`) 43 | var matchVersionInfoHelperRegx = regexp.MustCompile(`\$(\d)`) 44 | 45 | func parseMatch(s string, soft bool) *Match { 46 | var m = &Match{} 47 | var regx *regexp.Regexp 48 | 49 | for _, r := range matchLoadRegexps { 50 | if r.MatchString(s) { 51 | regx = r 52 | } 53 | } 54 | 55 | if regx == nil { 56 | panic(errors.New("match 语句参数不正确")) 57 | } 58 | 59 | args := regx.FindStringSubmatch(s) 60 | m.Soft = soft 61 | m.Service = args[1] 62 | m.Service = FixProtocol(m.Service) 63 | m.Pattern = args[2] 64 | m.PatternRegexp = m.getPatternRegexp(m.Pattern, args[3]) 65 | m.VersionInfo = &FingerPrint{ 66 | ProbeName: "", 67 | MatchRegexString: "", 68 | Service: m.Service, 69 | ProductName: m.getVersionInfo(s, "PRODUCTNAME"), 70 | Version: m.getVersionInfo(s, "VERSION"), 71 | Info: m.getVersionInfo(s, "INFO"), 72 | Hostname: m.getVersionInfo(s, "HOSTNAME"), 73 | OperatingSystem: m.getVersionInfo(s, "OS"), 74 | DeviceType: m.getVersionInfo(s, "DEVICE"), 75 | CPEs: m.getCPEInfo(s), 76 | CPEAttributes: m.getCPEAttributes(s), 77 | } 78 | return m 79 | } 80 | 81 | func (m *Match) getPatternRegexp(pattern string, opt string) *regexp2.Regexp { 82 | pattern = strings.ReplaceAll(pattern, `\0`, `\x00`) 83 | if opt != "" { 84 | if strings.Contains(opt, "i") == false { 85 | opt += "i" 86 | } 87 | if pattern[:1] == "^" { 88 | pattern = fmt.Sprintf("^(?%s:%s", opt, pattern[1:]) 89 | } else { 90 | pattern = fmt.Sprintf("(?%s:%s", opt, pattern) 91 | } 92 | if pattern[len(pattern)-1:] == "$" { 93 | pattern = fmt.Sprintf("%s)$", pattern[:len(pattern)-1]) 94 | } else { 95 | pattern = fmt.Sprintf("%s)", pattern) 96 | } 97 | } 98 | //pattern = regexp.MustCompile(`\\x[89a-f][0-9a-f]`).ReplaceAllString(pattern,".") 99 | regex, err := regexp2.Compile(pattern, regexp2.None) 100 | if err != nil { 101 | panic(err) 102 | } 103 | return regex 104 | } 105 | 106 | func (m *Match) getVersionInfo(s string, regID string) string { 107 | if matchVersionInfoRegexps[regID].MatchString(s) { 108 | return matchVersionInfoRegexps[regID].FindStringSubmatch(s)[1] 109 | } else { 110 | return "" 111 | } 112 | } 113 | 114 | // getCPEInfo 提取match语句中的所有CPE条目 115 | func (m *Match) getCPEInfo(s string) []string { 116 | return matchCPERegexp.FindAllString(s, -1) 117 | } 118 | 119 | // getCPEAttributes 解析CPE条目为Attributes结构体 120 | func (m *Match) getCPEAttributes(s string) []*common.Attributes { 121 | cpeStrings := m.getCPEInfo(s) 122 | var attributes []*common.Attributes 123 | 124 | for _, cpeStr := range cpeStrings { 125 | attr := common.NewAttributesWithCPE(cpeStr) 126 | if attr != nil { 127 | attributes = append(attributes, attr) 128 | } 129 | } 130 | 131 | return attributes 132 | } 133 | 134 | func (m *Match) makeVersionInfo(s string, f *FingerPrint) { 135 | f.Info = m.makeVersionInfoSubHelper(s, m.VersionInfo.Info) 136 | f.DeviceType = m.makeVersionInfoSubHelper(s, m.VersionInfo.DeviceType) 137 | f.Hostname = m.makeVersionInfoSubHelper(s, m.VersionInfo.Hostname) 138 | f.OperatingSystem = m.makeVersionInfoSubHelper(s, m.VersionInfo.OperatingSystem) 139 | f.ProductName = m.makeVersionInfoSubHelper(s, m.VersionInfo.ProductName) 140 | f.Version = m.makeVersionInfoSubHelper(s, m.VersionInfo.Version) 141 | f.Service = m.makeVersionInfoSubHelper(s, m.VersionInfo.Service) 142 | 143 | // 处理CPE信息,支持变量替换 144 | f.CPEs = m.makeVersionInfoCPEHelper(s, m.VersionInfo.CPEs) 145 | f.CPEAttributes = m.makeVersionInfoCPEAttributesHelper(s, f.CPEs) 146 | } 147 | 148 | func (m *Match) makeVersionInfoSubHelper(s string, pattern string) string { 149 | match, _ := m.PatternRegexp.FindStringMatch(s) 150 | if match == nil { 151 | return pattern 152 | } 153 | 154 | // 构建匹配组数组 155 | var sArr []string 156 | sArr = append(sArr, match.String()) // 完整匹配 157 | for i := 1; i < match.GroupCount(); i++ { 158 | group := match.GroupByNumber(i) 159 | if group != nil { 160 | sArr = append(sArr, group.String()) 161 | } else { 162 | sArr = append(sArr, "") 163 | } 164 | } 165 | 166 | if len(sArr) == 1 { 167 | return pattern 168 | } 169 | if pattern == "" { 170 | return pattern 171 | } 172 | 173 | if matchVersionInfoHelperRegxP.MatchString(pattern) { 174 | pattern = matchVersionInfoHelperRegxP.ReplaceAllStringFunc(pattern, func(repl string) string { 175 | a := matchVersionInfoHelperRegxP.FindStringSubmatch(repl)[1] 176 | return "$" + a 177 | }) 178 | } 179 | 180 | if matchVersionInfoHelperRegx.MatchString(pattern) { 181 | pattern = matchVersionInfoHelperRegx.ReplaceAllStringFunc(pattern, func(repl string) string { 182 | i, _ := strconv.Atoi(matchVersionInfoHelperRegx.FindStringSubmatch(repl)[1]) 183 | return sArr[i] 184 | }) 185 | } 186 | pattern = strings.ReplaceAll(pattern, "\n", "") 187 | pattern = strings.ReplaceAll(pattern, "\r", "") 188 | return pattern 189 | } 190 | 191 | // makeVersionInfoCPEHelper 处理CPE列表,支持变量替换 192 | func (m *Match) makeVersionInfoCPEHelper(s string, cpePatterns []string) []string { 193 | var processedCPEs []string 194 | 195 | for _, cpePattern := range cpePatterns { 196 | processedCPE := m.makeVersionInfoSubHelper(s, cpePattern) 197 | if processedCPE != "" { 198 | processedCPEs = append(processedCPEs, processedCPE) 199 | } 200 | } 201 | 202 | return processedCPEs 203 | } 204 | 205 | // makeVersionInfoCPEAttributesHelper 将处理后的CPE字符串转换为Attributes 206 | func (m *Match) makeVersionInfoCPEAttributesHelper(s string, cpeStrings []string) []*common.Attributes { 207 | var attributes []*common.Attributes 208 | 209 | for _, cpeStr := range cpeStrings { 210 | attr := common.NewAttributesWithCPE(cpeStr) 211 | if attr != nil { 212 | attributes = append(attributes, attr) 213 | } 214 | } 215 | 216 | return attributes 217 | } 218 | -------------------------------------------------------------------------------- /fingers/rules.go: -------------------------------------------------------------------------------- 1 | package fingers 2 | 3 | import ( 4 | "bytes" 5 | "github.com/chainreactors/utils/encode" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | type Regexps struct { 11 | Body []string `yaml:"body,omitempty" json:"body,omitempty" jsonschema:"title=Body Patterns,description=String patterns to match in HTTP response body,nullable,example=nginx"` 12 | MD5 []string `yaml:"md5,omitempty" json:"md5,omitempty" jsonschema:"title=MD5 Hashes,description=MD5 hashes of response bodies to match,nullable,pattern=^[a-f0-9]{32}$,example=d41d8cd98f00b204e9800998ecf8427e"` 13 | MMH3 []string `yaml:"mmh3,omitempty" json:"mmh3,omitempty" jsonschema:"title=MMH3 Hashes,description=MurmurHash3 hashes for favicon matching,nullable,example=116323821"` 14 | Regexp []string `yaml:"regexp,omitempty" json:"regexp,omitempty" jsonschema:"title=Regular Expressions,description=Regex patterns for advanced matching,nullable,example=nginx/([\\d\\.]+)"` 15 | Version []string `yaml:"version,omitempty" json:"version,omitempty" jsonschema:"title=Version Patterns,description=Regex patterns to extract version information,nullable,example=([\\d\\.]+)"` 16 | Cert []string `yaml:"cert,omitempty" json:"cert,omitempty" jsonschema:"title=Certificate Patterns,description=Patterns to match in SSL certificates,nullable,example=nginx"` 17 | CompliedRegexp []*regexp.Regexp `yaml:"-" json:"-"` 18 | CompiledVulnRegexp []*regexp.Regexp `yaml:"-" json:"-"` 19 | CompiledVersionRegexp []*regexp.Regexp `yaml:"-" json:"-"` 20 | FingerName string `yaml:"-" json:"-"` 21 | Header []string `yaml:"header,omitempty" json:"header,omitempty" jsonschema:"title=Header Patterns,description=Patterns to match in HTTP headers,nullable,example=Server: nginx"` 22 | Vuln []string `yaml:"vuln,omitempty" json:"vuln,omitempty" jsonschema:"title=Vulnerability Patterns,description=Regex patterns indicating security vulnerabilities,nullable,example=admin/config.php"` 23 | } 24 | 25 | func (r *Regexps) Compile(caseSensitive bool) error { 26 | for _, reg := range r.Regexp { 27 | creg, err := compileRegexp("(?i)" + reg) 28 | if err != nil { 29 | return err 30 | } 31 | r.CompliedRegexp = append(r.CompliedRegexp, creg) 32 | } 33 | 34 | for _, reg := range r.Vuln { 35 | creg, err := compileRegexp("(?i)" + reg) 36 | if err != nil { 37 | return err 38 | } 39 | r.CompiledVulnRegexp = append(r.CompiledVulnRegexp, creg) 40 | } 41 | 42 | for _, reg := range r.Version { 43 | creg, err := compileRegexp(reg) 44 | if err != nil { 45 | return err 46 | } 47 | r.CompiledVersionRegexp = append(r.CompiledVersionRegexp, creg) 48 | } 49 | 50 | for i, b := range r.Body { 51 | if !caseSensitive { 52 | r.Body[i] = strings.ToLower(b) 53 | } 54 | } 55 | 56 | for i, h := range r.Header { 57 | if !caseSensitive { 58 | r.Header[i] = strings.ToLower(h) 59 | } 60 | } 61 | return nil 62 | } 63 | 64 | type Favicons struct { 65 | Path string `yaml:"path,omitempty" json:"path,omitempty" jsonschema:"title=Favicon Path,description=Path to the favicon file,nullable,example=/favicon.ico"` 66 | Mmh3 []string `yaml:"mmh3,omitempty" json:"mmh3,omitempty" jsonschema:"title=MMH3 Hashes,description=MurmurHash3 hashes of favicon content,nullable,example=116323821"` 67 | Md5 []string `yaml:"md5,omitempty" json:"md5,omitempty" jsonschema:"title=MD5 Hashes,description=MD5 hashes of favicon content,nullable,pattern=^[a-f0-9]{32}$,example=d41d8cd98f00b204e9800998ecf8427e"` 68 | } 69 | 70 | type Rule struct { 71 | Version string `yaml:"version,omitempty" json:"version,omitempty" jsonschema:"title=Version,description=Version string or extraction pattern,nullable,example=1.18.0"` 72 | Favicon *Favicons `yaml:"favicon,omitempty" json:"favicon,omitempty" jsonschema:"title=Favicon Rules,description=Favicon-based matching rules,nullable"` 73 | Regexps *Regexps `yaml:"regexps,omitempty" json:"regexps,omitempty" jsonschema:"title=Regex Rules,description=Regular expression matching rules,nullable"` 74 | SendDataStr string `yaml:"send_data,omitempty" json:"send_data,omitempty" jsonschema:"title=Send Data,description=Data to send for active probing,nullable,example=GET /admin HTTP/1.1\\r\\nHost: {{Hostname}}\\r\\n\\r\\n"` 75 | SendData senddata `yaml:"-" json:"-"` 76 | Info string `yaml:"info,omitempty" json:"info,omitempty" jsonschema:"title=Information,description=Additional information about the detection,nullable,example=Admin panel detected"` 77 | Vuln string `yaml:"vuln,omitempty" json:"vuln,omitempty" jsonschema:"title=Vulnerability,description=Vulnerability information if detected,nullable,example=Default admin credentials"` 78 | Level int `yaml:"level,omitempty" json:"level,omitempty" jsonschema:"title=Detection Level,description=Active probing level (0=passive 1+=active),minimum=0,maximum=5,default=0,example=1"` 79 | FingerName string `yaml:"-" json:"-"` 80 | IsActive bool `yaml:"-" json:"-"` 81 | } 82 | 83 | func (r *Rule) Compile(name string, caseSensitive bool) error { 84 | if r.Version == "" { 85 | r.Version = "_" 86 | } 87 | r.FingerName = name 88 | if r.SendDataStr != "" { 89 | r.SendData, _ = encode.DSLParser(r.SendDataStr) 90 | if r.Level == 0 { 91 | r.Level = 1 92 | } 93 | r.IsActive = true 94 | } 95 | 96 | if r.Regexps != nil { 97 | err := r.Regexps.Compile(caseSensitive) 98 | if err != nil { 99 | return err 100 | } 101 | } 102 | 103 | return nil 104 | } 105 | 106 | type Rules []*Rule 107 | 108 | func (rs Rules) Compile(name string, caseSensitive bool) error { 109 | for _, r := range rs { 110 | err := r.Compile(name, caseSensitive) 111 | if err != nil { 112 | return err 113 | } 114 | } 115 | return nil 116 | } 117 | 118 | func (r *Rule) Match(content, header, body []byte) (bool, bool, string) { 119 | // 漏洞匹配优先 120 | for _, reg := range r.Regexps.CompiledVulnRegexp { 121 | res, ok := compiledMatch(reg, content) 122 | if ok { 123 | return true, true, res 124 | } 125 | } 126 | 127 | // 正则匹配 128 | for _, reg := range r.Regexps.CompliedRegexp { 129 | res, ok := compiledMatch(reg, content) 130 | if ok { 131 | FingerLog.Debugf("%s finger hit, regexp: %q", r.FingerName, reg.String()) 132 | return true, false, res 133 | } 134 | } 135 | 136 | // http头匹配, http协议特有的匹配 137 | if header != nil { 138 | for _, headerStr := range r.Regexps.Header { 139 | if bytes.Contains(header, []byte(headerStr)) { 140 | FingerLog.Debugf("%s finger hit, header: %s", r.FingerName, headerStr) 141 | return true, false, "" 142 | } 143 | } 144 | } 145 | 146 | if body == nil && header == nil { 147 | body = content 148 | } 149 | 150 | // body匹配 151 | for _, bodyReg := range r.Regexps.Body { 152 | if bytes.Contains(body, []byte(bodyReg)) { 153 | FingerLog.Debugf("%s finger hit, body: %q", r.FingerName, bodyReg) 154 | return true, false, "" 155 | } 156 | } 157 | 158 | // MD5 匹配 159 | for _, md5s := range r.Regexps.MD5 { 160 | if md5s == encode.Md5Hash(body) { 161 | FingerLog.Debugf("%s finger hit, md5: %s", r.FingerName, md5s) 162 | return true, false, "" 163 | } 164 | } 165 | 166 | // mmh3 匹配 167 | for _, mmh3s := range r.Regexps.MMH3 { 168 | if mmh3s == encode.Mmh3Hash32(body) { 169 | FingerLog.Debugf("%s finger hit, mmh3: %s", r.FingerName, mmh3s) 170 | return true, false, "" 171 | } 172 | } 173 | 174 | return false, false, "" 175 | } 176 | 177 | func (r *Rule) MatchCert(content string) bool { 178 | for _, cert := range r.Regexps.Cert { 179 | if strings.Contains(content, cert) { 180 | return true 181 | } 182 | } 183 | return false 184 | } 185 | -------------------------------------------------------------------------------- /common/framework.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/chainreactors/utils/iutils" 5 | "strings" 6 | ) 7 | 8 | // 指纹类型定义 9 | type FingerprintType int 10 | 11 | const ( 12 | WebFingerprint FingerprintType = iota // Web应用指纹 13 | ServiceFingerprint // 服务指纹 14 | ) 15 | 16 | // 引擎能力定义 17 | type EngineCapability struct { 18 | SupportWeb bool // 支持Web指纹 19 | SupportService bool // 支持Service指纹 20 | } 21 | 22 | // Service指纹检测结果 23 | type ServiceResult struct { 24 | Framework *Framework 25 | Vuln *Vuln // 可选,只有部分引擎(如fingers)会返回漏洞信息 26 | } 27 | 28 | var NoGuess bool 29 | 30 | type From int 31 | 32 | const ( 33 | FrameFromDefault From = iota 34 | FrameFromACTIVE 35 | FrameFromICO 36 | FrameFromNOTFOUND 37 | FrameFromGUESS 38 | FrameFromRedirect 39 | FrameFromFingers 40 | FrameFromFingerprintHub 41 | FrameFromWappalyzer 42 | FrameFromEhole 43 | FrameFromGoby 44 | FrameFromNmap 45 | ) 46 | 47 | func (f From) String() string { 48 | return FrameFromMap[f] 49 | } 50 | 51 | var FrameFromMap = map[From]string{ 52 | FrameFromDefault: "default", 53 | FrameFromACTIVE: "active", 54 | FrameFromICO: "ico", 55 | FrameFromNOTFOUND: "404", 56 | FrameFromGUESS: "guess", 57 | FrameFromRedirect: "redirect", 58 | FrameFromFingers: "fingers", 59 | FrameFromFingerprintHub: "fingerprinthub", 60 | FrameFromWappalyzer: "wappalyzer", 61 | FrameFromEhole: "ehole", 62 | FrameFromGoby: "goby", 63 | FrameFromNmap: "nmap", 64 | } 65 | 66 | func GetFrameFrom(s string) From { 67 | switch s { 68 | case "active": 69 | return FrameFromACTIVE 70 | case "404": 71 | return FrameFromNOTFOUND 72 | case "ico": 73 | return FrameFromICO 74 | case "guess": 75 | return FrameFromGUESS 76 | case "redirect": 77 | return FrameFromRedirect 78 | case "fingerprinthub", "fingerprinthub_v4": // fingerprinthub_v4 保留用于向后兼容 79 | return FrameFromFingerprintHub 80 | case "wappalyzer": 81 | return FrameFromWappalyzer 82 | case "ehole": 83 | return FrameFromEhole 84 | case "goby": 85 | return FrameFromGoby 86 | case "fingers": 87 | return FrameFromFingers 88 | case "nmap": 89 | return FrameFromNmap 90 | 91 | default: 92 | return FrameFromDefault 93 | } 94 | } 95 | 96 | func NewFramework(name string, from From) *Framework { 97 | frame := &Framework{ 98 | Name: name, 99 | From: from, 100 | Froms: map[From]bool{from: true}, 101 | Tags: make([]string, 0), 102 | Attributes: NewAttributesWithAny(), 103 | } 104 | frame.Attributes.Product = name 105 | frame.Attributes.Part = "a" 106 | if from >= FrameFromFingers { 107 | frame.AddTag(from.String()) 108 | } 109 | return frame 110 | } 111 | 112 | func NewFrameworkWithVersion(name string, from From, version string) *Framework { 113 | frame := NewFramework(name, from) 114 | frame.Attributes.Version = version 115 | //frame.Version = version 116 | return frame 117 | } 118 | 119 | type Framework struct { 120 | Name string `json:"name"` 121 | From From `json:"-"` // 指纹可能会有多个来源, 指纹合并时会将多个来源记录到froms中 122 | Froms map[From]bool `json:"froms,omitempty"` 123 | Tags []string `json:"tags,omitempty"` 124 | IsFocus bool `json:"is_focus,omitempty"` 125 | *Attributes `json:"attributes,omitempty"` 126 | } 127 | 128 | func (f *Framework) String() string { 129 | var s strings.Builder 130 | if f.IsFocus { 131 | s.WriteString("focus:") 132 | } 133 | s.WriteString(f.Name) 134 | 135 | if f.Version != "" { 136 | s.WriteString(":" + strings.Replace(f.Version, ":", "_", -1)) 137 | } 138 | 139 | if len(f.Froms) > 1 { 140 | s.WriteString(":(") 141 | var froms []string 142 | for from, _ := range f.Froms { 143 | froms = append(froms, FrameFromMap[from]) 144 | } 145 | s.WriteString(strings.Join(froms, " ")) 146 | s.WriteString(")") 147 | } else { 148 | for from, _ := range f.Froms { 149 | if from != FrameFromFingers { 150 | s.WriteString(":") 151 | s.WriteString(FrameFromMap[from]) 152 | } 153 | } 154 | } 155 | return strings.TrimSpace(s.String()) 156 | } 157 | 158 | func (f *Framework) UpdateAttributes(attrs *Attributes) { 159 | if f.Version != "" { 160 | attrs.Version = f.Version 161 | } 162 | f.Attributes = attrs 163 | } 164 | 165 | func (f *Framework) CPE() string { 166 | return f.Attributes.String() 167 | } 168 | 169 | func (f *Framework) URI() string { 170 | return f.Attributes.URI() 171 | } 172 | 173 | func (f *Framework) WFN() string { 174 | return f.Attributes.WFNString() 175 | } 176 | 177 | func (f *Framework) IsGuess() bool { 178 | var is bool 179 | for from, _ := range f.Froms { 180 | if from == FrameFromGUESS { 181 | is = true 182 | } else { 183 | return false 184 | } 185 | } 186 | return is 187 | } 188 | 189 | func (f *Framework) AddTag(tag string) { 190 | if !f.HasTag(tag) { 191 | f.Tags = append(f.Tags, tag) 192 | } 193 | } 194 | 195 | func (f *Framework) HasTag(tag string) bool { 196 | for _, t := range f.Tags { 197 | if t == tag { 198 | return true 199 | } 200 | } 201 | return false 202 | } 203 | 204 | type Frameworks map[string]*Framework 205 | 206 | func (fs Frameworks) One() *Framework { 207 | for _, f := range fs { 208 | return f 209 | } 210 | return nil 211 | } 212 | 213 | func (fs Frameworks) List() []*Framework { 214 | var frameworks []*Framework 215 | for _, f := range fs { 216 | frameworks = append(frameworks, f) 217 | } 218 | return frameworks 219 | } 220 | 221 | func (fs Frameworks) Add(other *Framework) bool { 222 | if other == nil { 223 | return false 224 | } 225 | other.Name = strings.ToLower(other.Name) 226 | if frame, ok := fs[other.Name]; ok { 227 | for from, _ := range other.Froms { 228 | frame.Froms[from] = true 229 | } 230 | frame.Tags = iutils.StringsUnique(append(frame.Tags, other.Tags...)) 231 | frame.UpdateAttributes(other.Attributes) 232 | return false 233 | } else { 234 | fs[other.Name] = other 235 | return true 236 | } 237 | } 238 | 239 | func (fs Frameworks) Merge(other Frameworks) int { 240 | // name, tag 统一小写, 减少指纹库之间的差异 241 | var n int 242 | for _, f := range other { 243 | f.Name = strings.ToLower(f.Name) 244 | if fs.Add(f) { 245 | n += 1 246 | } 247 | } 248 | return n 249 | } 250 | 251 | func (fs Frameworks) String() string { 252 | if fs == nil { 253 | return "" 254 | } 255 | frameworkStrs := make([]string, len(fs)) 256 | i := 0 257 | for _, f := range fs { 258 | if NoGuess && f.IsGuess() { 259 | continue 260 | } 261 | frameworkStrs[i] = f.String() 262 | i++ 263 | } 264 | return strings.Join(frameworkStrs, "||") 265 | } 266 | 267 | func (fs Frameworks) GetNames() []string { 268 | if fs == nil { 269 | return nil 270 | } 271 | var titles []string 272 | for _, f := range fs { 273 | if !f.IsGuess() { 274 | titles = append(titles, f.Name) 275 | } 276 | } 277 | return titles 278 | } 279 | 280 | func (fs Frameworks) URI() []string { 281 | if fs == nil { 282 | return nil 283 | } 284 | var uris []string 285 | for _, f := range fs { 286 | uris = append(uris, f.URI()) 287 | } 288 | return uris 289 | } 290 | 291 | func (fs Frameworks) CPE() []string { 292 | if fs == nil { 293 | return nil 294 | } 295 | var cpes []string 296 | for _, f := range fs { 297 | cpes = append(cpes, f.CPE()) 298 | } 299 | return cpes 300 | } 301 | 302 | func (fs Frameworks) WFN() []string { 303 | if fs == nil { 304 | return nil 305 | } 306 | var wfns []string 307 | for _, f := range fs { 308 | wfns = append(wfns, f.WFN()) 309 | } 310 | return wfns 311 | } 312 | 313 | func (fs Frameworks) IsFocus() bool { 314 | if fs == nil { 315 | return false 316 | } 317 | for _, f := range fs { 318 | if f.IsFocus { 319 | return true 320 | } 321 | } 322 | return false 323 | } 324 | 325 | func (fs Frameworks) HasTag(tag string) bool { 326 | for _, f := range fs { 327 | if f.HasTag(tag) { 328 | return true 329 | } 330 | } 331 | return false 332 | } 333 | 334 | func (fs Frameworks) HasFrom(from string) bool { 335 | for _, f := range fs { 336 | if f.Froms[GetFrameFrom(from)] { 337 | return true 338 | } 339 | } 340 | return false 341 | } 342 | -------------------------------------------------------------------------------- /fingers/engine.go: -------------------------------------------------------------------------------- 1 | package fingers 2 | 3 | import ( 4 | "errors" 5 | "github.com/chainreactors/fingers/common" 6 | "github.com/chainreactors/fingers/favicon" 7 | "github.com/chainreactors/fingers/resources" 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | const ( 12 | HTTPProtocol = "http" 13 | TCPProtocol = "tcp" 14 | UDPProtocol = "udp" 15 | ) 16 | 17 | func NewFingersEngineWithCustom(httpConfig, socketConfig []byte) (*FingersEngine, error) { 18 | if httpConfig != nil { 19 | resources.FingersHTTPData = httpConfig 20 | } 21 | if socketConfig != nil { 22 | resources.FingersSocketData = socketConfig 23 | } 24 | return NewFingersEngine(resources.FingersHTTPData, resources.FingersSocketData, resources.PortData) 25 | } 26 | 27 | func NewFingersEngine(httpData, socketData, portData []byte) (*FingersEngine, error) { 28 | // httpdata must be not nil 29 | // socketdata can be nil 30 | if resources.PrePort == nil && portData != nil { 31 | // 临时设置 PortData 以供 LoadPorts 使用 32 | resources.PortData = portData 33 | _, err := resources.LoadPorts() 34 | if err != nil { 35 | return nil, err 36 | } 37 | } 38 | 39 | httpfs, err := LoadFingers(httpData) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | engine := &FingersEngine{ 45 | HTTPFingers: httpfs, 46 | Favicons: favicon.NewFavicons(), 47 | } 48 | 49 | engine.SocketFingers, err = LoadFingers(socketData) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | err = engine.Compile() 55 | if err != nil { 56 | return nil, err 57 | } 58 | return engine, nil 59 | } 60 | 61 | type FingersEngine struct { 62 | HTTPFingers Fingers 63 | HTTPFingersActiveFingers Fingers 64 | SocketFingers Fingers 65 | SocketGroup FingerMapper 66 | Favicons *favicon.FaviconsEngine 67 | } 68 | 69 | func (engine *FingersEngine) Name() string { 70 | return "fingers" 71 | } 72 | 73 | func (engine *FingersEngine) Len() int { 74 | return len(engine.HTTPFingers) + len(engine.SocketFingers) 75 | } 76 | 77 | // addToSocketGroup 将指纹添加到SocketGroup中 78 | func (engine *FingersEngine) addToSocketGroup(f *Finger) { 79 | if engine.SocketGroup == nil { 80 | engine.SocketGroup = make(FingerMapper) 81 | } 82 | if f.DefaultPort != nil { 83 | for _, port := range resources.PrePort.ParsePortSlice(f.DefaultPort) { 84 | engine.SocketGroup[port] = append(engine.SocketGroup[port], f) 85 | } 86 | } else { 87 | engine.SocketGroup["0"] = append(engine.SocketGroup["0"], f) 88 | } 89 | } 90 | 91 | func (engine *FingersEngine) Compile() error { 92 | var err error 93 | if engine.HTTPFingers == nil { 94 | return errors.New("fingers is nil") 95 | } 96 | for _, finger := range engine.HTTPFingers { 97 | err = finger.Compile(false) 98 | if err != nil { 99 | return err 100 | } 101 | if finger.IsActive { 102 | engine.HTTPFingersActiveFingers = append(engine.HTTPFingersActiveFingers, finger) 103 | } 104 | } 105 | 106 | //初始化favicon规则 107 | for _, finger := range engine.HTTPFingers { 108 | for _, rule := range finger.Rules { 109 | if rule.Favicon != nil { 110 | for _, mmh3 := range rule.Favicon.Mmh3 { 111 | engine.Favicons.Mmh3Fingers[mmh3] = finger.Name 112 | } 113 | for _, md5 := range rule.Favicon.Md5 { 114 | engine.Favicons.Md5Fingers[md5] = finger.Name 115 | } 116 | } 117 | } 118 | } 119 | 120 | if engine.SocketFingers != nil { 121 | for _, finger := range engine.SocketFingers { 122 | err = finger.Compile(true) 123 | if err != nil { 124 | return err 125 | } 126 | engine.addToSocketGroup(finger) 127 | } 128 | } 129 | return nil 130 | } 131 | 132 | func (engine *FingersEngine) Append(fingers Fingers) error { 133 | for _, f := range fingers { 134 | err := f.Compile(false) 135 | if err != nil { 136 | return err 137 | } 138 | if f.Protocol == HTTPProtocol { 139 | engine.HTTPFingers = append(engine.HTTPFingers, f) 140 | if f.IsActive { 141 | engine.HTTPFingersActiveFingers = append(engine.HTTPFingersActiveFingers, f) 142 | } 143 | } else if f.Protocol == TCPProtocol { 144 | engine.SocketFingers = append(engine.SocketFingers, f) 145 | engine.addToSocketGroup(f) 146 | } 147 | } 148 | return nil 149 | } 150 | 151 | // LoadFromYAML loads fingerprints from YAML file or URL and appends them to the engine 152 | // This method only supports YAML format for custom fingerprints 153 | func (engine *FingersEngine) LoadFromYAML(path string) error { 154 | content, err := resources.LoadFingersFromYAML(path) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | var fingers Fingers 160 | if err := yaml.Unmarshal(content, &fingers); err != nil { 161 | return err 162 | } 163 | 164 | return engine.Append(fingers) 165 | } 166 | 167 | func (engine *FingersEngine) SocketMatch(content []byte, port string, level int, sender Sender, callback Callback) (*common.Framework, *common.Vuln) { 168 | // socket service only match one fingerprint 169 | var alreadyFrameworks = make(map[string]bool) 170 | input := NewContent(content, "", false) 171 | var fs common.Frameworks 172 | var vs common.Vulns 173 | if port != "" { 174 | fs, vs = engine.SocketGroup[port].Match(input, level, sender, callback, true) 175 | if len(fs) > 0 { 176 | return fs.One(), vs.One() 177 | } 178 | for _, fs := range engine.SocketGroup[port] { 179 | alreadyFrameworks[fs.Name] = true 180 | } 181 | } 182 | 183 | fs, vs = engine.SocketGroup["0"].Match(input, level, sender, callback, true) 184 | if len(fs) > 0 { 185 | return fs.One(), vs.One() 186 | } 187 | for _, fs := range engine.SocketGroup["0"] { 188 | alreadyFrameworks[fs.Name] = true 189 | } 190 | 191 | for _, fs := range engine.SocketGroup { 192 | for _, finger := range fs { 193 | if _, ok := alreadyFrameworks[finger.Name]; ok { 194 | continue 195 | } else { 196 | alreadyFrameworks[finger.Name] = true 197 | } 198 | 199 | frame, vuln, ok := finger.Match(input, level, sender) 200 | if ok { 201 | if callback != nil { 202 | callback(frame, vuln) 203 | } 204 | return frame, vuln 205 | } 206 | } 207 | } 208 | return nil, nil 209 | } 210 | 211 | // WebMatch 实现Web指纹匹配 212 | func (engine *FingersEngine) WebMatch(content []byte) common.Frameworks { 213 | fs, _ := engine.HTTPMatch(content, "") 214 | return fs 215 | } 216 | 217 | // ServiceMatch 实现Service指纹匹配 218 | func (engine *FingersEngine) ServiceMatch(host string, portStr string, level int, sender common.ServiceSender, callback common.ServiceCallback) *common.ServiceResult { 219 | if sender == nil { 220 | return nil 221 | } 222 | 223 | // 创建自适应的Callback 224 | fingersCallback := func(framework *common.Framework, vuln *common.Vuln) { 225 | if callback != nil { 226 | result := &common.ServiceResult{ 227 | Framework: framework, 228 | Vuln: vuln, 229 | } 230 | callback(result) 231 | } 232 | } 233 | 234 | // 创建适配器将common.ServiceSender转换为fingers.Sender 235 | // fingers.Sender: func([]byte) ([]byte, bool) 236 | // common.ServiceSender.Send(host, port, data) ([]byte, error) 237 | fingersSender := Sender(func(data []byte) ([]byte, bool) { 238 | response, err := sender.Send(host, portStr, data, "tcp") 239 | if err != nil { 240 | return nil, false 241 | } 242 | return response, true 243 | }) 244 | 245 | framework, vuln := engine.SocketMatch(nil, portStr, level, fingersSender, fingersCallback) 246 | 247 | return &common.ServiceResult{ 248 | Framework: framework, 249 | Vuln: vuln, 250 | } 251 | } 252 | 253 | func (engine *FingersEngine) Capability() common.EngineCapability { 254 | return common.EngineCapability{ 255 | SupportWeb: true, // fingers支持Web指纹 256 | SupportService: true, // fingers支持Service指纹 257 | } 258 | } 259 | 260 | func (engine *FingersEngine) HTTPMatch(content []byte, cert string) (common.Frameworks, common.Vulns) { 261 | // input map[string]interface{} 262 | // content: []byte 263 | // cert: string 264 | return engine.HTTPFingers.PassiveMatch(NewContent(content, cert, true), false) 265 | } 266 | 267 | func (engine *FingersEngine) HTTPActiveMatch(level int, sender Sender, callback Callback) (common.Frameworks, common.Vulns) { 268 | return engine.HTTPFingersActiveFingers.ActiveMatch(level, sender, callback, false) 269 | } 270 | -------------------------------------------------------------------------------- /fingers/fingers.go: -------------------------------------------------------------------------------- 1 | package fingers 2 | 3 | import ( 4 | "github.com/chainreactors/fingers/common" 5 | "github.com/chainreactors/logs" 6 | "github.com/chainreactors/utils" 7 | ) 8 | 9 | var ( 10 | OPSEC = false 11 | FingerLog = logs.Log 12 | ) 13 | 14 | type Finger struct { 15 | Name string `yaml:"name" json:"name" jsonschema:"required,title=Fingerprint Name,description=Unique identifier for the fingerprint,example=nginx"` 16 | Attributes common.Attributes `yaml:",inline" json:",inline"` 17 | Author string `yaml:"author,omitempty" json:"author,omitempty" jsonschema:"title=Author,description= Finger template author,nullable"` 18 | Description string `yaml:"description,omitempty" json:"description,omitempty" jsonschema:"title=Description,description= Finger template description,nullable"` 19 | Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty" jsonschema:"title=Protocol,description=Network protocol type,nullable,enum=http,enum=tcp,enum=udp,default=http,example=http"` 20 | Link string `yaml:"link,omitempty" json:"link,omitempty" jsonschema:"title=Link,description=Reference URL for the software,nullable,format=uri,example=https://nginx.org"` 21 | DefaultPort []string `yaml:"default_port,omitempty" json:"default_port,omitempty" jsonschema:"title=Default Ports,description=Default ports used by this service,nullable,example=80,example=443"` 22 | Focus bool `yaml:"focus,omitempty" json:"focus,omitempty" jsonschema:"title=Focus,description=Whether this is a high-priority fingerprint,default=false"` 23 | Rules Rules `yaml:"rule,omitempty" json:"rule,omitempty" jsonschema:"required,title=Rules,description=Matching rules for fingerprint detection"` 24 | Tags []string `yaml:"tag,omitempty" json:"tag,omitempty" jsonschema:"title=Tags,description=Category tags for classification,nullable,example=web,example=server"` 25 | Level int `yaml:"level,omitempty" json:"level,omitempty" jsonschema:"title=Level,description=Fingerprint detection level,default=0"` 26 | Opsec bool `yaml:"opsec,omitempty" json:"opsec,omitempty" jsonschema:"title=OPSEC,description=Whether this fingerprint uses operational security measures,default=false"` 27 | IsActive bool `yaml:"-" json:"-"` 28 | } 29 | 30 | func (finger *Finger) Compile(caseSensitive bool) error { 31 | if finger.Protocol == "" { 32 | finger.Protocol = HTTPProtocol 33 | } 34 | 35 | if len(finger.DefaultPort) == 0 { 36 | if finger.Protocol == HTTPProtocol { 37 | finger.DefaultPort = []string{"80"} 38 | } 39 | } else if utils.PrePort != nil { 40 | finger.DefaultPort = utils.ParsePortsSlice(finger.DefaultPort) 41 | } 42 | 43 | err := finger.Rules.Compile(finger.Name, caseSensitive) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | for _, r := range finger.Rules { 49 | if r.IsActive { 50 | finger.IsActive = true 51 | break 52 | } 53 | } 54 | return nil 55 | } 56 | 57 | func (finger *Finger) ToResult(hasFrame, hasVuln bool, ver string, index int) (frame *common.Framework, vuln *common.Vuln) { 58 | if index >= len(finger.Rules) { 59 | return nil, nil 60 | } 61 | 62 | if hasFrame { 63 | if ver != "" { 64 | frame = common.NewFrameworkWithVersion(finger.Name, common.FrameFromFingers, ver) 65 | } else if finger.Rules[index].Version != "_" { 66 | frame = common.NewFrameworkWithVersion(finger.Name, common.FrameFromFingers, finger.Rules[index].Version) 67 | } else { 68 | frame = common.NewFramework(finger.Name, common.FrameFromFingers) 69 | } 70 | } 71 | 72 | if hasVuln { 73 | if finger.Rules[index].Vuln != "" { 74 | vuln = &common.Vuln{Name: finger.Rules[index].Vuln, SeverityLevel: HIGH, Framework: frame} 75 | } else if finger.Rules[index].Info != "" { 76 | vuln = &common.Vuln{Name: finger.Rules[index].Info, SeverityLevel: INFO, Framework: frame} 77 | } else { 78 | vuln = &common.Vuln{Name: finger.Name, SeverityLevel: INFO} 79 | } 80 | if finger.IsActive { 81 | vuln.Detail = map[string][]string{"path": []string{finger.Rules[index].SendDataStr}} 82 | } 83 | } 84 | 85 | frame.Vendor = finger.Attributes.Vendor 86 | frame.Product = finger.Attributes.Product 87 | return frame, vuln 88 | } 89 | 90 | func (finger *Finger) Match(content *Content, level int, sender Sender) (*common.Framework, *common.Vuln, bool) { 91 | // sender用来处理需要主动发包的场景, 因为不通工具中的传入指不相同, 因此采用闭包的方式自定义result进行处理, 并允许添加更多的功能. 92 | // 例如在spray中, sender可以用来配置header等, 也可以进行特定的path拼接 93 | // 如果sender留空只进行被动的指纹判断, 将无视rules中的senddata字段 94 | 95 | for i, rule := range finger.Rules { 96 | var ishttp bool 97 | var isactive bool 98 | if finger.Protocol == HTTPProtocol { 99 | ishttp = true 100 | } 101 | var c []byte 102 | var ok bool 103 | // 主动发包获取指纹 104 | if level >= rule.Level && rule.SendData != nil && sender != nil { 105 | if OPSEC == true && finger.Opsec == true { 106 | FingerLog.Debugf("(opsec!!!) skip active finger %s scan", finger.Name) 107 | } else { 108 | c, ok = sender(rule.SendData) 109 | if ok { 110 | isactive = true 111 | if ishttp { 112 | content.UpdateContent(c) 113 | } else { 114 | content.Content = c 115 | } 116 | } 117 | } 118 | } 119 | hasFrame, hasVuln, ver := RuleMatcher(rule, content, ishttp) 120 | if hasFrame { 121 | frame, vuln := finger.ToResult(hasFrame, hasVuln, ver, i) 122 | if finger.Focus { 123 | frame.IsFocus = true 124 | } 125 | //if vuln == nil && isactive { 126 | // vuln = &parsers.Vuln{Name: finger.Name + " detect", SeverityLevel: INFO, Detail: map[string]interface{}{"path": rule.SendDataStr}} 127 | //} 128 | 129 | if isactive { 130 | frame.From = common.FrameFromFingers 131 | frame.Froms = map[common.From]bool{common.FrameFromACTIVE: true} 132 | } 133 | for _, tag := range finger.Tags { 134 | frame.AddTag(tag) 135 | } 136 | return frame, vuln, true 137 | } 138 | } 139 | return nil, nil, false 140 | } 141 | 142 | func (finger *Finger) PassiveMatch(content *Content) (*common.Framework, *common.Vuln, bool) { 143 | for i, rule := range finger.Rules { 144 | var ishttp bool 145 | if finger.Protocol == HTTPProtocol { 146 | ishttp = true 147 | } 148 | 149 | hasFrame, hasVuln, ver := RuleMatcher(rule, content, ishttp) 150 | if hasFrame { 151 | frame, vuln := finger.ToResult(hasFrame, hasVuln, ver, i) 152 | if finger.Focus { 153 | frame.IsFocus = true 154 | } 155 | //if vuln == nil && isactive { 156 | // vuln = &common.Vuln{Name: finger.Name + " detect", SeverityLevel: INFO, Detail: map[string]interface{}{"path": rule.SendDataStr}} 157 | //} 158 | 159 | for _, tag := range finger.Tags { 160 | frame.AddTag(tag) 161 | } 162 | return frame, vuln, true 163 | } 164 | } 165 | return nil, nil, false 166 | } 167 | 168 | func (finger *Finger) ActiveMatch(level int, sender Sender) (*common.Framework, *common.Vuln, bool) { 169 | if sender == nil { 170 | return nil, nil, false 171 | } 172 | 173 | for i, rule := range finger.Rules { 174 | var ishttp bool 175 | if finger.Protocol == HTTPProtocol { 176 | ishttp = true 177 | } 178 | // 主动发包获取指纹 179 | if !(level >= rule.Level && rule.SendData != nil) { 180 | return nil, nil, false 181 | } 182 | if OPSEC == true && finger.Opsec == true { 183 | FingerLog.Debugf("(opsec!!!) skip active finger %s scan", finger.Name) 184 | return nil, nil, false 185 | } 186 | content := &Content{} 187 | c, ok := sender(rule.SendData) 188 | if ok { 189 | if ishttp { 190 | content.UpdateContent(c) 191 | } else { 192 | content.Content = c 193 | } 194 | } else { 195 | return nil, nil, false 196 | } 197 | 198 | hasFrame, hasVuln, ver := RuleMatcher(rule, content, ishttp) 199 | if hasFrame { 200 | frame, vuln := finger.ToResult(hasFrame, hasVuln, ver, i) 201 | if finger.Focus { 202 | frame.IsFocus = true 203 | } 204 | //if vuln == nil && isactive { 205 | // vuln = &common.Vuln{Name: finger.Name + " detect", SeverityLevel: INFO, Detail: map[string]interface{}{"path": rule.SendDataStr}} 206 | //} 207 | 208 | frame.From = common.FrameFromFingers 209 | frame.Froms = map[common.From]bool{common.FrameFromACTIVE: true} 210 | for _, tag := range finger.Tags { 211 | frame.AddTag(tag) 212 | } 213 | return frame, vuln, true 214 | } 215 | } 216 | return nil, nil, false 217 | } 218 | -------------------------------------------------------------------------------- /resources/aliases.yaml: -------------------------------------------------------------------------------- 1 | - name: tomcat 2 | vendor: apache 3 | product: tomcat 4 | alias: 5 | fingers: 6 | - tomcat 7 | ehole: 8 | - Apache Tomcat 9 | goby: 10 | - Apache-Tomcat 11 | fingerprinthub: 12 | - apache-tomcat 13 | wappalyzer: 14 | - Apache Tomcat 15 | tanggo: 16 | - tomcat默认页面 17 | - apachetomcat 18 | - apache_tomcat 19 | - name: block tanggo 20 | vendor: null 21 | product: null 22 | block: 23 | - tanggo 24 | alias: 25 | tanggo: 26 | - html5 27 | - javadoc 28 | - name: discuz 29 | vendor: discuz 30 | product: discuz 31 | alias: 32 | fingers: 33 | - discuz 34 | ehole: 35 | - Discuz! 36 | goby: 37 | - Discuz 38 | fingerprinthub: 39 | - discuz 40 | wappalyzer: 41 | - Discuz! 42 | - name: 用友 NC 43 | vendor: yonyou 44 | product: NC 45 | alias: 46 | fingers: 47 | - 用友NC 48 | ehole: 49 | - 用友NC 50 | - YONYOU NC 51 | goby: 52 | - UFIDA NC 53 | fingerprinthub: 54 | - yonyou-ufida-nc 55 | 56 | - name: 用友 GRP-U8 57 | vendor: yonyou 58 | product: GRP-U8 59 | alias: 60 | fingers: 61 | - 用友 GRP-U8 62 | ehole: 63 | - 用友 GRP-U8 64 | goby: 65 | - 用友GRP-U8 66 | fingerprinthub: 67 | - yonyou-grp-u8 68 | 69 | - name: 用友 U8 Cloud 70 | vendor: yonyou 71 | product: U8 Cloud 72 | alias: 73 | fingers: 74 | - 用友 U8 Cloud 75 | ehole: 76 | - 用友 U8 Cloud 77 | fingerprinthub: 78 | - yonyou-u8-cloud 79 | 80 | - name: 用友 RMIS 81 | vendor: yonyou 82 | product: RMIS 83 | alias: 84 | fingers: 85 | - 用友 RMIS 86 | fingerprinthub: 87 | - yonyou-rmis 88 | 89 | - name: 用友 TurboCRM 90 | vendor: yonyou 91 | product: TurboCRM 92 | alias: 93 | fingers: 94 | - 用友 TurboCRM 95 | fingerprinthub: 96 | - yonyou-turbocrm 97 | 98 | - name: 用友 Seeyon OA 99 | vendor: yonyou 100 | product: Seeyon OA 101 | alias: 102 | fingers: 103 | - 用友 Seeyon OA 104 | fingerprinthub: 105 | - yonyou-seeyon-oa 106 | 107 | - name: 泛微 e-cology 108 | vendor: weaver 109 | product: e-cology 110 | alias: 111 | fingers: 112 | - 泛微 e-cology 113 | ehole: 114 | - 泛微 e-cology 115 | goby: 116 | - 泛微-协同办公OA 117 | fingerprinthub: 118 | - ecology泛微e-office 119 | 120 | - name: 泛微 e-mobile 121 | vendor: weaver 122 | product: e-mobile 123 | alias: 124 | fingers: 125 | - 泛微 e-mobile 126 | ehole: 127 | - 泛微 e-mobile 128 | goby: 129 | - 泛微-移动办公OA 130 | fingerprinthub: 131 | - ecology泛微e-mobile 132 | 133 | - name: 泛微 云桥e-bridge 134 | vendor: weaver 135 | product: e-bridge 136 | alias: 137 | fingers: 138 | - 泛微 云桥e-bridge 139 | ehole: 140 | - 泛微 云桥e-bridge 141 | fingerprinthub: 142 | - ecology泛微云桥e-bridge 143 | 144 | - name: 蓝凌 OA 145 | vendor: landray 146 | product: OA 147 | alias: 148 | fingers: 149 | - 蓝凌 OA 150 | ehole: 151 | - 蓝凌 OA 152 | goby: 153 | - LandRay-OA system 154 | fingerprinthub: 155 | - landray-oa 156 | 157 | - name: 通达 OA 158 | vendor: tongda 159 | product: OA 160 | alias: 161 | fingers: 162 | - 通达 OA 163 | ehole: 164 | - 通达 OA 165 | goby: 166 | - Tongda-OA 167 | fingerprinthub: 168 | - tongda-oa 169 | 170 | - name: 金蝶云 星空 171 | vendor: kingdee 172 | product: 星空 173 | alias: 174 | fingers: 175 | - 金蝶云星空 176 | ehole: 177 | - 金蝶云星空 178 | goby: 179 | - Kingdee Cloud 180 | fingerprinthub: 181 | - kingdee-cloud 182 | 183 | - name: 帆软 FineReport 184 | vendor: fanruan 185 | product: FineReport 186 | alias: 187 | fingers: 188 | - 帆软 FineReport 189 | ehole: 190 | - 帆软 FineReport 191 | goby: 192 | - FineReport 193 | fingerprinthub: 194 | - fanruan-finereport 195 | 196 | - name: 万户 OA 197 | vendor: whir 198 | product: OA 199 | alias: 200 | fingers: 201 | - 万户 OA 202 | ehole: 203 | - 万户 OA 204 | goby: 205 | - Whir-OA 206 | fingerprinthub: 207 | - whir-oa 208 | 209 | - name: 华天 动力OA 210 | vendor: huatian 211 | product: 动力OA 212 | alias: 213 | fingers: 214 | - 华天 动力OA 215 | ehole: 216 | - 华天 动力OA 217 | goby: 218 | - Huatian-OA 219 | fingerprinthub: 220 | - huatian-oa 221 | 222 | - name: 红帆 OA 223 | vendor: hongfan 224 | product: OA 225 | alias: 226 | fingers: 227 | - 红帆 OA 228 | ehole: 229 | - 红帆 OA 230 | goby: 231 | - Hongfan-OA 232 | fingerprinthub: 233 | - hongfan-oa 234 | - name: 华天动力协同OA办公系统 235 | vendor: huatian 236 | product: collaborative 237 | alias: 238 | fingers: 239 | - 华天动力协同OA办公系统 240 | - 华天动力OA 241 | ehole: 242 | - 华天动力OA 243 | fingerprinthub: 244 | - 华天动力协同OA办公系统 245 | 246 | - name: IBOS企业协同管理软件 247 | vendor: IBOS 248 | product: collaborative 249 | alias: 250 | fingers: 251 | - IBOS企业协同管理软件 252 | ehole: 253 | - IBOS企业协同管理软件 254 | 255 | - name: 信达OA 256 | vendor: xdoa 257 | product: OA 258 | alias: 259 | fingers: 260 | - 信达OA 261 | fingerprinthub: 262 | - 信达OA 263 | 264 | - name: 协众OA 265 | vendor: cnoa 266 | product: OA 267 | alias: 268 | fingers: 269 | - 协众OA 270 | ehole: 271 | - 协众OA 272 | goby: 273 | - CNOA 274 | fingerprinthub: 275 | - 协众OA 276 | 277 | - name: 京伦OA 278 | vendor: jinglun 279 | product: OA 280 | alias: 281 | fingers: 282 | - 京伦OA 283 | fingerprinthub: 284 | - 京伦OA 285 | 286 | - name: 中望OA 287 | vendor: xtoa 288 | product: OA 289 | alias: 290 | fingers: 291 | - 中望OA 292 | fingerprinthub: 293 | - 中望OA 294 | 295 | - name: 泛普软件 建筑工程施工OA 296 | vendor: phpcms 297 | product: construction 298 | alias: 299 | fingers: 300 | - 泛普软件 建筑工程施工OA 301 | ehole: 302 | - 泛普 建筑工程施工OA 303 | goby: 304 | - Phpcms 305 | 306 | - name: 凤凰办公OA 307 | vendor: fenghuang 308 | product: OA 309 | alias: 310 | fingers: 311 | - 凤凰办公OA 312 | fingerprinthub: 313 | - 凤凰办公OA 314 | 315 | - name: 九思OA 316 | vendor: jiusi 317 | product: OA 318 | alias: 319 | fingers: 320 | - 九思OA 321 | - 九思协同办公系统 322 | fingerprinthub: 323 | - 九思OA 324 | 325 | - name: 东华OA系统 326 | vendor: donghua 327 | product: OA 328 | alias: 329 | fingers: 330 | - 东华OA系统 331 | fingerprinthub: 332 | - 东华OA系统 333 | 334 | - name: LemonOA 335 | vendor: mossle 336 | product: OA 337 | alias: 338 | fingers: 339 | - LemonOA 340 | fingerprinthub: 341 | - LemonOA 342 | 343 | - name: PHPOA 344 | vendor: phpoa 345 | product: OA 346 | alias: 347 | fingers: 348 | - PHPOA 349 | ehole: 350 | - PHPOA 351 | fingerprinthub: 352 | - PHPOA 353 | 354 | - name: 山大鲁能 OA 355 | vendor: sanda 356 | product: OA 357 | alias: 358 | fingers: 359 | - 山大鲁能 OA 360 | fingerprinthub: 361 | - 山大鲁能 OA 362 | 363 | - name: 协达 OA 364 | vendor: xietong 365 | product: OA 366 | alias: 367 | fingers: 368 | - 协达 OA 369 | fingerprinthub: 370 | - 协达 OA 371 | 372 | - name: TopOffice 373 | vendor: topoffice 374 | product: OA 375 | alias: 376 | fingers: 377 | - TopOffice 378 | fingerprinthub: 379 | - TopOffice 380 | 381 | - name: CTOP OA 382 | vendor: ctop 383 | product: OA 384 | alias: 385 | fingers: 386 | - CTOP OA 387 | fingerprinthub: 388 | - CTOP OA 389 | 390 | - name: 网康 NS-ASG应用安全网关 391 | vendor: networktech 392 | product: NS-ASG 393 | alias: 394 | fingers: 395 | - 网康 NS-ASG应用安全网关 396 | fingerprinthub: 397 | - 网康 NS-ASG应用安全网关 398 | 399 | - name: 红帆 Ioffice 400 | vendor: hongfan 401 | product: Ioffice 402 | alias: 403 | fingers: 404 | - ioffice 405 | fingerprinthub: 406 | - 红帆 Ioffice 407 | 408 | - name: redis 409 | vendor: Redis 410 | product: Redis 411 | alias: 412 | goby: 413 | - Redis 414 | block: 415 | - goby 416 | 417 | - name: ibm-db2 database 418 | vendor: ibm 419 | product: db2 420 | alias: 421 | goby: 422 | - IBM-DB2 database 423 | block: 424 | - goby 425 | -------------------------------------------------------------------------------- /engine_test.go: -------------------------------------------------------------------------------- 1 | package fingers 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "runtime/pprof" 10 | "strconv" 11 | "testing" 12 | "time" 13 | 14 | "github.com/chainreactors/fingers/common" 15 | "github.com/chainreactors/fingers/ehole" 16 | "github.com/chainreactors/fingers/fingerprinthub" 17 | "github.com/chainreactors/fingers/fingers" 18 | "github.com/chainreactors/fingers/goby" 19 | "github.com/chainreactors/fingers/resources" 20 | "github.com/chainreactors/fingers/wappalyzer" 21 | "github.com/chainreactors/utils/httputils" 22 | ) 23 | 24 | func TestEngine(t *testing.T) { 25 | // 创建内存分析文件 26 | memProfileFile, err := os.Create("memprofile.out") 27 | if err != nil { 28 | t.Fatal("could not create memory profile: ", err) 29 | } 30 | defer memProfileFile.Close() 31 | 32 | // 在检测 `DetectContent` 前进行内存分析 33 | 34 | // Your test code 35 | engine, err := NewEngine() 36 | if err != nil { 37 | panic(err) 38 | } 39 | fmt.Println(engine.String()) 40 | 41 | //client := &http.Client{ 42 | // Transport: &http.Transport{ 43 | // TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 44 | // }, 45 | //} 46 | //resp, err := client.Get("https://boce.aliyun.com/detect/http") 47 | //if err != nil { 48 | // panic(err) 49 | //} 50 | //start := time.Now() 51 | //content := httputils.ReadRaw(resp) 52 | 53 | // 调用 DetectContent 54 | 55 | content, err := os.ReadFile("1.raw") 56 | if err != nil { 57 | return 58 | } 59 | frames, err := engine.DetectContent(content) 60 | if err != nil { 61 | return 62 | } 63 | 64 | // 打印执行时间 65 | //println("耗时: " + time.Since(start).String()) 66 | fmt.Println(frames.String()) 67 | 68 | // 在检测 `DetectContent` 后进行内存分析 69 | pprof.WriteHeapProfile(memProfileFile) 70 | 71 | // 打印内存分配 72 | for _, f := range frames { 73 | fmt.Println("cpe: ", f.CPE(), "||||", f.String()) 74 | } 75 | } 76 | 77 | func TestEngine_Match(t *testing.T) { 78 | engine, err := NewEngine() 79 | if err != nil { 80 | panic(err) 81 | } 82 | resp, err := http.Get("http://127.0.0.1:8089") 83 | if err != nil { 84 | panic(err) 85 | } 86 | frames := engine.Match(resp) 87 | fmt.Println(frames.String()) 88 | } 89 | 90 | func TestFavicon(t *testing.T) { 91 | engine, err := NewEngine() 92 | if err != nil { 93 | panic(err) 94 | } 95 | resp, err := http.Get("http://baidu.com/favicon.ico") 96 | if err != nil { 97 | return 98 | } 99 | content := httputils.ReadRaw(resp) 100 | body, _, _ := httputils.SplitHttpRaw(content) 101 | frame := engine.DetectFavicon(body) 102 | fmt.Println(frame) 103 | } 104 | 105 | func TestFingersEngine(t *testing.T) { 106 | engine, err := fingers.NewFingersEngine(resources.FingersHTTPData, resources.FingersSocketData, resources.PortData) 107 | if err != nil { 108 | t.Error(err) 109 | } 110 | client := &http.Client{ 111 | Transport: &http.Transport{ 112 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 113 | }, 114 | } 115 | resp, err := client.Get("https://218.94.127.25:443") 116 | if err != nil { 117 | t.Log(err) 118 | return 119 | } 120 | fmt.Println("math") 121 | 122 | content := httputils.ReadRaw(resp) 123 | frames := engine.WebMatch(content) 124 | for _, frame := range frames { 125 | t.Log(frame) 126 | } 127 | } 128 | 129 | func TestEngine_MatchWithEngines(t *testing.T) { 130 | engine, err := NewEngine() 131 | if err != nil { 132 | t.Error(err) 133 | } 134 | resp, err := http.Get("http://127.0.0.1") 135 | if err != nil { 136 | return 137 | } 138 | 139 | need := []string{FingersEngine, FingerPrintEngine} 140 | frames := engine.MatchWithEngines(resp, need...) 141 | for _, frame := range frames { 142 | t.Log(frame) 143 | } 144 | } 145 | 146 | func TestFingerPrintHubsEngine(t *testing.T) { 147 | engine, err := fingerprinthub.NewFingerPrintHubEngine(resources.FingerprinthubWebData, resources.FingerprinthubServiceData) 148 | if err != nil { 149 | t.Error(err) 150 | } 151 | resp, err := http.Get("http://127.0.0.1") 152 | if err != nil { 153 | return 154 | } 155 | 156 | content := httputils.ReadRaw(resp) 157 | frames := engine.WebMatch(content) 158 | for _, frame := range frames { 159 | t.Log(frame) 160 | } 161 | } 162 | 163 | func TestEHoleEngine(t *testing.T) { 164 | engine, err := ehole.NewEHoleEngine(resources.EholeData) 165 | if err != nil { 166 | t.Error(err) 167 | } 168 | resp, err := http.Get("http://127.0.0.1:8089") 169 | if err != nil { 170 | return 171 | } 172 | 173 | content := httputils.ReadRaw(resp) 174 | header, body, ok := httputils.SplitHttpRaw(content) 175 | if ok { 176 | frames := engine.MatchWithHeaderAndBody(string(header), string(body)) 177 | for _, frame := range frames { 178 | t.Log(frame) 179 | } 180 | } 181 | } 182 | 183 | func TestGobyEngine(t *testing.T) { 184 | engine, err := goby.NewGobyEngine(resources.GobyData) 185 | if err != nil { 186 | t.Error(err) 187 | } 188 | resp, err := http.Get("https://baidu.com") 189 | if err != nil { 190 | return 191 | } 192 | 193 | content := httputils.ReadRaw(resp) 194 | content = bytes.ToLower(content) 195 | start := time.Now() 196 | frames := engine.WebMatch(content) 197 | fmt.Println(frames) 198 | fmt.Println(time.Since(start).String()) 199 | } 200 | 201 | func TestEngine_Wappalyzer(t *testing.T) { 202 | engine, err := wappalyzer.NewWappalyzeEngine(resources.WappalyzerData) 203 | if err != nil { 204 | t.Error(err) 205 | return 206 | } 207 | resp, err := http.Get("http://127.0.0.1:8000") 208 | if err != nil { 209 | return 210 | } 211 | 212 | content := httputils.ReadBody(resp) 213 | start := time.Now() 214 | frames := engine.Fingerprint(resp.Header, content) 215 | fmt.Println(frames) 216 | fmt.Println(time.Since(start).String()) 217 | } 218 | 219 | func TestAlias(t *testing.T) { 220 | engine, err := NewEngine() 221 | if err != nil { 222 | t.Error() 223 | return 224 | } 225 | fmt.Println(engine.FindAny("cdncache_server")) 226 | fmt.Println(engine.Aliases.Aliases["cdn-cache-server"]) 227 | fmt.Println(engine.Aliases.Map["fingers"]["cdn-cache-server"]) 228 | } 229 | 230 | func TestNmapEngine(t *testing.T) { 231 | engine, err := NewEngine(NmapEngine) 232 | if err != nil { 233 | t.Error(err) 234 | return 235 | } 236 | 237 | nmapEngine := engine.Nmap() 238 | if nmapEngine == nil { 239 | t.Error("nmap engine not found") 240 | return 241 | } 242 | 243 | fmt.Printf("nmap engine loaded with %d fingerprints\n", nmapEngine.Len()) 244 | 245 | // 测试Service指纹匹配 - 使用common包的默认实现 246 | testServiceSender := common.NewServiceSender(3 * time.Second) 247 | 248 | testServiceCallback := func(result *common.ServiceResult) { 249 | if result.Framework != nil { 250 | t.Logf("detected service: %s", result.Framework.String()) 251 | } 252 | } 253 | 254 | result := nmapEngine.ServiceMatch("127.0.0.1", "80", 1, testServiceSender, testServiceCallback) 255 | if result != nil && result.Framework != nil { 256 | t.Logf("service result: %s", result.Framework.String()) 257 | } 258 | 259 | // 测试引擎能力 260 | capability := nmapEngine.Capability() 261 | if !capability.SupportService { 262 | t.Error("nmap engine should support service fingerprinting") 263 | } 264 | if capability.SupportWeb { 265 | t.Error("nmap engine should not support web fingerprinting") 266 | } 267 | 268 | // 测试WebMatch应该返回空结果 269 | webFrames := nmapEngine.WebMatch([]byte("test")) 270 | if len(webFrames) != 0 { 271 | t.Error("nmap engine WebMatch should return empty results") 272 | } 273 | } 274 | 275 | // TestServiceEngine 测试Service引擎的能力 276 | func TestServiceEngine(t *testing.T) { 277 | engine, err := NewEngine(NmapEngine) 278 | if err != nil { 279 | t.Error(err) 280 | return 281 | } 282 | 283 | // 测试获取支持Service的引擎 284 | serviceEngines := engine.GetEnginesByType(common.ServiceFingerprint) 285 | expectedServiceEngines := []string{NmapEngine} 286 | 287 | if len(serviceEngines) != len(expectedServiceEngines) { 288 | t.Errorf("Expected %d service engines, got %d", len(expectedServiceEngines), len(serviceEngines)) 289 | } 290 | 291 | for _, expected := range expectedServiceEngines { 292 | found := false 293 | for _, actual := range serviceEngines { 294 | if actual == expected { 295 | found = true 296 | break 297 | } 298 | } 299 | if !found { 300 | t.Errorf("Expected service engine %s not found", expected) 301 | } 302 | } 303 | 304 | // 测试DetectService API 305 | testSender := common.NewServiceSender(3 * time.Second) 306 | 307 | ports := []int{80, 443, 445, 135, 1080, 3306, 1433, 1521} 308 | for _, port := range ports { 309 | results, err := engine.DetectService("127.0.0.1", strconv.Itoa(port), 9, testSender, nil) 310 | if err != nil { 311 | t.Logf("DetectService error: %v", err) 312 | } 313 | if len(results) > 0 && results[0].Framework != nil { 314 | fmt.Printf("DetectService result: %s\n", results[0].Framework.String()) 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /wappalyzer/cmd/update-fingerprints/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net/http" 11 | "os" 12 | "reflect" 13 | "sort" 14 | "strings" 15 | ) 16 | 17 | var fingerprints = flag.String("fingerprints", "../../fingerprints_data.json", "File to write wappalyzer fingerprints to") 18 | 19 | // Fingerprints contains a map of fingerprints for tech detection 20 | type Fingerprints struct { 21 | // Apps is organized as 22 | Apps map[string]Fingerprint `json:"technologies"` 23 | } 24 | 25 | // Fingerprint is a single piece of information about a tech 26 | type Fingerprint struct { 27 | Cats []int `json:"cats"` 28 | CSS interface{} `json:"css"` 29 | Cookies map[string]string `json:"cookies"` 30 | JS map[string]string `json:"js"` 31 | Headers map[string]string `json:"headers"` 32 | HTML interface{} `json:"html"` 33 | Script interface{} `json:"scripts"` 34 | ScriptSrc interface{} `json:"scriptSrc"` 35 | Meta map[string]interface{} `json:"meta"` 36 | Implies interface{} `json:"implies"` 37 | Description string `json:"description"` 38 | Website string `json:"website"` 39 | CPE string `json:"cpe"` 40 | } 41 | 42 | // OutputFingerprints contains a map of fingerprints for tech detection 43 | // optimized and validated for the tech detection package 44 | type OutputFingerprints struct { 45 | // Apps is organized as 46 | Apps map[string]OutputFingerprint `json:"apps"` 47 | } 48 | 49 | // OutputFingerprint is a single piece of information about a tech validated and normalized 50 | type OutputFingerprint struct { 51 | Cats []int `json:"cats,omitempty"` 52 | CSS []string `json:"css,omitempty"` 53 | Cookies map[string]string `json:"cookies,omitempty"` 54 | JS []string `json:"js,omitempty"` 55 | Headers map[string]string `json:"headers,omitempty"` 56 | HTML []string `json:"html,omitempty"` 57 | Script []string `json:"scripts,omitempty"` 58 | ScriptSrc []string `json:"scriptSrc,omitempty"` 59 | Meta map[string][]string `json:"meta,omitempty"` 60 | Implies []string `json:"implies,omitempty"` 61 | Description string `json:"description,omitempty"` 62 | Website string `json:"website,omitempty"` 63 | CPE string `json:"cpe,omitempty"` 64 | } 65 | 66 | const fingerprintURL = "https://raw.githubusercontent.com/Lissy93/wapalyzer/master/src/technologies/%s.json" 67 | 68 | func makeFingerprintURLs() []string { 69 | files := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "_"} 70 | 71 | fingerprints := make([]string, 0, len(files)) 72 | for _, item := range files { 73 | fingerprints = append(fingerprints, fmt.Sprintf(fingerprintURL, item)) 74 | } 75 | return fingerprints 76 | } 77 | 78 | func main() { 79 | flag.Parse() 80 | 81 | fingerprintURLs := makeFingerprintURLs() 82 | 83 | fingerprintsOld := &Fingerprints{ 84 | Apps: make(map[string]Fingerprint), 85 | } 86 | for _, fingerprintItem := range fingerprintURLs { 87 | if err := gatherFingerprintsFromURL(fingerprintItem, fingerprintsOld); err != nil { 88 | log.Fatalf("Could not gather fingerprints %s: %v\n", fingerprintItem, err) 89 | } 90 | } 91 | 92 | log.Printf("Read fingerprints from the server\n") 93 | log.Printf("Starting normalizing of %d fingerprints...\n", len(fingerprintsOld.Apps)) 94 | 95 | outputFingerprints := normalizeFingerprints(fingerprintsOld) 96 | 97 | log.Printf("Got %d valid fingerprints\n", len(outputFingerprints.Apps)) 98 | 99 | fingerprintsFile, err := os.OpenFile(*fingerprints, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666) 100 | if err != nil { 101 | log.Fatalf("Could not open fingerprints file %s: %s\n", *fingerprints, err) 102 | } 103 | 104 | // sort map keys and pretty print the json to make git diffs useful 105 | 106 | data, err := json.MarshalIndent(outputFingerprints, "", " ") 107 | if err != nil { 108 | log.Fatalf("Could not marshal fingerprints: %s\n", err) 109 | } 110 | _, err = fingerprintsFile.Write(data) 111 | if err != nil { 112 | log.Fatalf("Could not write fingerprints file: %s\n", err) 113 | } 114 | err = fingerprintsFile.Close() 115 | if err != nil { 116 | log.Fatalf("Could not close fingerprints file: %s\n", err) 117 | } 118 | } 119 | 120 | func gatherFingerprintsFromURL(URL string, fingerprints *Fingerprints) error { 121 | req, err := http.NewRequest(http.MethodGet, URL, nil) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | resp, err := http.DefaultClient.Do(req) 127 | if err != nil { 128 | return err 129 | } 130 | defer resp.Body.Close() 131 | 132 | data, err := io.ReadAll(resp.Body) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | fingerprintsOld := &Fingerprints{} 138 | err = json.NewDecoder(bytes.NewReader(data)).Decode(&fingerprintsOld.Apps) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | for k, v := range fingerprintsOld.Apps { 144 | fingerprints.Apps[k] = v 145 | } 146 | return nil 147 | } 148 | 149 | func normalizeFingerprints(fingerprints *Fingerprints) *OutputFingerprints { 150 | outputFingerprints := &OutputFingerprints{Apps: make(map[string]OutputFingerprint)} 151 | 152 | for app, fingerprint := range fingerprints.Apps { 153 | output := OutputFingerprint{ 154 | Cats: fingerprint.Cats, 155 | Cookies: make(map[string]string), 156 | Headers: make(map[string]string), 157 | Meta: make(map[string][]string), 158 | Description: fingerprint.Description, 159 | Website: fingerprint.Website, 160 | CPE: fingerprint.CPE, 161 | } 162 | 163 | for cookie, value := range fingerprint.Cookies { 164 | output.Cookies[strings.ToLower(cookie)] = strings.ToLower(value) 165 | } 166 | for js := range fingerprint.JS { 167 | output.JS = append(output.JS, strings.ToLower(js)) 168 | } 169 | sort.Strings(output.JS) 170 | 171 | for header, pattern := range fingerprint.Headers { 172 | output.Headers[strings.ToLower(header)] = strings.ToLower(pattern) 173 | } 174 | 175 | // Use reflection type switch for determining HTML tag type 176 | if fingerprint.HTML != nil { 177 | v := reflect.ValueOf(fingerprint.HTML) 178 | 179 | switch v.Kind() { 180 | case reflect.String: 181 | data := v.Interface().(string) 182 | output.HTML = append(output.HTML, strings.ToLower(data)) 183 | case reflect.Slice: 184 | data := v.Interface().([]interface{}) 185 | 186 | for _, pattern := range data { 187 | pat := pattern.(string) 188 | output.HTML = append(output.HTML, strings.ToLower(pat)) 189 | } 190 | } 191 | 192 | sort.Strings(output.HTML) 193 | } 194 | 195 | // Use reflection type switch for determining Script type 196 | if fingerprint.Script != nil { 197 | v := reflect.ValueOf(fingerprint.Script) 198 | 199 | switch v.Kind() { 200 | case reflect.String: 201 | data := v.Interface().(string) 202 | output.Script = append(output.Script, strings.ToLower(data)) 203 | case reflect.Slice: 204 | data := v.Interface().([]interface{}) 205 | for _, pattern := range data { 206 | pat := pattern.(string) 207 | output.Script = append(output.Script, strings.ToLower(pat)) 208 | } 209 | } 210 | 211 | sort.Strings(output.Script) 212 | } 213 | 214 | // Use reflection type switch for determining ScriptSrc type 215 | if fingerprint.ScriptSrc != nil { 216 | v := reflect.ValueOf(fingerprint.ScriptSrc) 217 | 218 | switch v.Kind() { 219 | case reflect.String: 220 | data := v.Interface().(string) 221 | output.ScriptSrc = append(output.ScriptSrc, strings.ToLower(data)) 222 | case reflect.Slice: 223 | data := v.Interface().([]interface{}) 224 | for _, pattern := range data { 225 | pat := pattern.(string) 226 | output.ScriptSrc = append(output.ScriptSrc, strings.ToLower(pat)) 227 | } 228 | } 229 | 230 | sort.Strings(output.ScriptSrc) 231 | } 232 | 233 | for header, pattern := range fingerprint.Meta { 234 | v := reflect.ValueOf(pattern) 235 | 236 | switch v.Kind() { 237 | case reflect.String: 238 | data := strings.ToLower(v.Interface().(string)) 239 | if data == "" { 240 | output.Meta[strings.ToLower(header)] = []string{} 241 | } else { 242 | output.Meta[strings.ToLower(header)] = []string{data} 243 | } 244 | case reflect.Slice: 245 | data := v.Interface().([]interface{}) 246 | 247 | final := []string{} 248 | for _, pattern := range data { 249 | pat := pattern.(string) 250 | final = append(final, strings.ToLower(pat)) 251 | } 252 | sort.Strings(final) 253 | output.Meta[strings.ToLower(header)] = final 254 | } 255 | } 256 | 257 | // Use reflection type switch for determining "Implies" tag type 258 | if fingerprint.Implies != nil { 259 | v := reflect.ValueOf(fingerprint.Implies) 260 | 261 | switch v.Kind() { 262 | case reflect.String: 263 | data := v.Interface().(string) 264 | output.Implies = append(output.Implies, data) 265 | case reflect.Slice: 266 | data := v.Interface().([]interface{}) 267 | for _, pattern := range data { 268 | pat := pattern.(string) 269 | output.Implies = append(output.Implies, pat) 270 | } 271 | } 272 | 273 | sort.Strings(output.Implies) 274 | } 275 | 276 | // Use reflection type switch for determining CSS tag type 277 | if fingerprint.CSS != nil { 278 | v := reflect.ValueOf(fingerprint.CSS) 279 | 280 | switch v.Kind() { 281 | case reflect.String: 282 | data := v.Interface().(string) 283 | output.CSS = append(output.CSS, data) 284 | case reflect.Slice: 285 | data := v.Interface().([]interface{}) 286 | for _, pattern := range data { 287 | pat := pattern.(string) 288 | output.CSS = append(output.CSS, pat) 289 | } 290 | } 291 | 292 | sort.Strings(output.CSS) 293 | } 294 | 295 | // Only add if the fingerprint is valid 296 | outputFingerprints.Apps[app] = output 297 | } 298 | return outputFingerprints 299 | } 300 | --------------------------------------------------------------------------------