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