fingerprintx 源码学习

项目介绍

项目地址:https://github.com/praetorian-inc/fingerprintx

fingerprintx 是一款端口识别工具,支持协议如下:

SERVICE TRANSPORT SERVICE TRANSPORT
HTTP TCP REDIS TCP
SSH TCP MQTT3 TCP
MODBUS TCP VNC TCP
TELNET TCP MQTT5 TCP
FTP TCP RSYNC TCP
SMB TCP RPC TCP
DNS TCP OracleDB TCP
SMTP TCP RTSP TCP
PostgreSQL TCP MQTT5 TCP (TLS)
RDP TCP HTTPS TCP (TLS)
POP3 TCP SMTPS TCP (TLS)
KAFKA TCP MQTT3 TCP (TLS)
MySQL TCP RDP TCP (TLS)
MSSQL TCP POP3S TCP (TLS)
LDAP TCP LDAPS TCP (TLS)
IMAP TCP IMAPS TCP (TLS)
SNMP UDP Kafka TCP (TLS)
OPENVPN UDP NETBIOS-NS UDP
IPSEC UDP DHCP UDP
STUN UDP NTP UDP
DNS UDP

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pkg
├── plugins
│   │   ├── plugins.go # 插件注册相关
│   │   ├── pluginutils
│   │   │   ├── error.go
│   │   │   └── requests.go # 向 conn 中写入数据 读取数据
│   │   ├── services # 各种服务的识别插件、都实现了插件接口
│   │   └── types.go # 插件接口、各种服务的详细数据结构体( 实现 Metadata 接口 )
│   ├── runner
│   │   ├── report.go # 输出格式 JSON CSV
│   │   ├── root.go # cobra cli 参数解析 程序入口
│   │   ├── target.go # 解析命令行参数的目标为 plugins.Target
│   │   ├── types.go # cliConfig 结构体 参数相关
│   │   └── utils.go # 一些工具类
│   ├── scan
│   │   ├── plugin_list.go # import 导入所有插件包 ( 插件注册 )
│   │   ├── scan_api.go # API 可以直接调用 ScanTargets 完成指纹识别
│   │   ├── simple_scan.go # 具体的识别逻辑
│   │   └── types.go # config 结构体

源码学习

fingerprintx 的端口指纹识别是以插件的形式完成的,感觉很整齐,这种类型的项目看起来也很舒服:

image-20240302201011679

这些识别插件都实现了插件接口:

1
2
3
4
5
6
7
type Plugin interface {
Run(net.Conn, time.Duration, Target) (*Service, error) // 识别的主逻辑
PortPriority(uint16) bool // 端口优先权
Name() string // 名称
Type() Protocol // 协议
Priority() int // 优先级端口
}

这里的 PortPriority 表示的是端口优先权,比如 80 对应的是 http 服务,那么 http 识别插件的 PortPriority(80) 就会返回 true。

Priority 是端口优先级,如果优先权未识别出端口,那就会按照服务的优先级进行指纹识别。

很好的思路,而且这种思路使用了这样的写法,很整齐,之前的 go-Portscan 虽然也是这种思路,但是写法和 fingerprintx 比起来就有点乱。

先看一个指纹识别插件,这是 HTTP 包下面的,它实现了 HTTP 和 HTTPS 这里只留 HTTP 的写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
type HTTPPlugin struct {
analyzer *wappalyzer.Wappalyze
}

const HTTP = "http"

func init() {
wappalyzerClient, err := wappalyzer.New()
if err != nil {
panic("unable to initialize wappalyzer library")
}
// 注册 http 插件
plugins.RegisterPlugin(&HTTPPlugin{analyzer: wappalyzerClient})
}

// http 对应可能的端口 => 端口优先权
var (
commonHTTPPorts = map[int]struct{}{
80: {},
3000: {},
4567: {},
5000: {},
8000: {},
8001: {},
8080: {},
8081: {},
8888: {},
9001: {},
9080: {},
9090: {},
9100: {},
}
)
// 优先权判断
func (p *HTTPPlugin) PortPriority(port uint16) bool {
_, ok := commonHTTPPorts[int(port)]
return ok
}
// 指纹识别的主函数
func (p *HTTPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) {
// 对于 HTTP 协议的识别 直接发送 HTTP 请求
req, err := http.NewRequest("GET", fmt.Sprintf("http://%s", conn.RemoteAddr().String()), nil)
if err != nil {
if errors.Is(err, syscall.ECONNREFUSED) {
return nil, nil
}
return nil, &utils.RequestError{Message: err.Error()}
}

if target.Host != "" {
req.Host = target.Host
}

// http client with custom dialier to use the provided net.Conn
client := http.Client{
Timeout: timeout,
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return conn, nil
},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}

resp, err := client.Do(req)
if err != nil {
return nil, &utils.RequestError{Message: err.Error()}
}
defer resp.Body.Close()
// 进行 WEB 指纹识别 wappalyzergo
technologies, _ := p.FingerprintResponse(resp)

payload := plugins.ServiceHTTP{
Status: resp.Status,
StatusCode: resp.StatusCode,
ResponseHeaders: resp.Header,
}
if len(technologies) > 0 {
payload.Technologies = technologies
}
// 使用 CreateServiceFrom 生成一个结果 fingerprintx 这个地方也很好
return plugins.CreateServiceFrom(target, payload, false, resp.Header.Get("Server"), plugins.TCP), nil
}

// TCP 类型的
func (p *HTTPPlugin) Type() plugins.Protocol {
return plugins.TCP
}

// 优先级第一
func (p *HTTPPlugin) Priority() int {
return 0
}


func (p *HTTPPlugin) Name() string {
return HTTP
}


func (p *HTTPPlugin) FingerprintResponse(resp *http.Response) ([]string, error) {
return fingerprint(resp, p.analyzer)
}

// 调用 wappalyzerClient 进行指纹识别
func fingerprint(resp *http.Response, analyzer *wappalyzer.Wappalyze) ([]string, error) {
var technologies []string
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
fingerprint := analyzer.Fingerprint(resp.Header, data)
for tech := range fingerprint {
technologies = append(technologies, tech)
}

return technologies, nil
}

指纹识别和之前的 go-Portscan 略有不同,go-Portscan 是统一的,只分了协议、发送动作、接收判断规则。而 fingerprintx 则是对每种协议都做了处理,而且还会获取其详细信息,比如这里的 http 就会进行指纹识别。

再说一下 plugins.CreateServiceFrom 函数,上面说了他对很多规则都做了详细信息的获取函数,但是这些信息的结构是不同的,比如 HTTP 这里就是 plugins.ServiceHTTP 结构,看看 fingerprintx 是如果去处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func CreateServiceFrom(target Target, m Metadata, tls bool, version string, transport Protocol) *Service {
service := Service{}
// 将指纹数据直接转换成 json plugins.ServiceHTTP 实现了 Metadata 接口
b, _ := json.Marshal(m)
service.Host = target.Host
service.IP = target.Address.Addr().String()
service.Port = int(target.Address.Port())
service.Protocol = m.Type()
service.Transport = strings.ToLower(transport.String())
service.Raw = json.RawMessage(b)
if version != "" {
service.Version = version
}
service.TLS = tls
return &service
}

Metadata 接口只有一个 Type 方法:

1
2
3
type Metadata interface {
Type() string
}

HTTP 的就是这个了:

1
2
3
4
5
6
type ServiceHTTP struct {
Status string `json:"status"` // e.g. "200 OK"
StatusCode int `json:"statusCode"` // e.g. 200
ResponseHeaders http.Header `json:"responseHeaders"`
Technologies []string `json:"technologies,omitempty"`
}

再后面输出的时候,都直接调用的是 Service 接口的 Metadata 方法,其按照不同的数据格式进行对应的 json 序列化,这样就解决了输出 json 格式的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (e Service) Metadata() Metadata {
switch e.Protocol {
case ProtoFTP:
var p ServiceFTP
_ = json.Unmarshal(e.Raw, &p)
return p
case ProtoPostgreSQL:
var p ServicePostgreSQL
_ = json.Unmarshal(e.Raw, &p)
return p
// ......
default:
var p ServiceUnknown
_ = json.Unmarshal(e.Raw, &p)
return p
}
}

然后再看看它具体的识别逻辑 simple_scan.go ,写的很清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func init() {
// 插件初始化
setupPlugins()
// TLS 的一些东西
cipherSuites := make([]uint16, 0)
for _, suite := range tls.CipherSuites() {
cipherSuites = append(cipherSuites, suite.ID)
}
for _, suite := range tls.InsecureCipherSuites() {
cipherSuites = append(cipherSuites, suite.ID)
}
tlsConfig.InsecureSkipVerify = true //nolint:gosec
tlsConfig.CipherSuites = cipherSuites
tlsConfig.MinVersion = tls.VersionTLS10
}

setupPlugins 这就是把 plugin_list.go 中 import 导入注册的插件进行分类、排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func setupPlugins() {
if len(sortedTCPPlugins) > 0 {
return
}
// 对插件进行分类 TCP、TCPLSP、UDP
sortedTCPPlugins = append(sortedTCPPlugins, plugins.Plugins[plugins.TCP]...)
sortedTCPTLSPlugins = append(sortedTCPTLSPlugins, plugins.Plugins[plugins.TCPTLS]...)
sortedUDPPlugins = append(sortedUDPPlugins, plugins.Plugins[plugins.UDP]...)
// 将插件按照优先级进行排序
sort.Slice(sortedTCPPlugins, func(i, j int) bool {
return sortedTCPPlugins[i].Priority() < sortedTCPPlugins[j].Priority()
})
sort.Slice(sortedUDPPlugins, func(i, j int) bool {
return sortedUDPPlugins[i].Priority() < sortedUDPPlugins[j].Priority()
})
sort.Slice(sortedTCPTLSPlugins, func(i, j int) bool {
return sortedTCPTLSPlugins[i].Priority() < sortedTCPTLSPlugins[j].Priority()
})
}

然后是识别的具体流程,很清晰,先遍历一圈,识别默认服务,然后再按照优先级进行指纹识别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
func (c *Config) SimpleScanTarget(target plugins.Target) (*plugins.Service, error) {
ip := target.Address.Addr().String()
port := target.Address.Port()
for _, plugin := range sortedTCPPlugins {
// 查看该端口对应的服务是否存在 存在的化就扫描这个服务 ( 端口优先权 )
if plugin.PortPriority(port) {
// TCP 连接
conn, err := DialTCP(ip, port)
if err != nil {
return nil, fmt.Errorf("unable to connect, err = %w", err)
}
// 使用 simplePluginRunner 进行识别
result, err := simplePluginRunner(conn, target, c, plugin)
if err != nil && c.Verbose {
log.Printf("error: %v scanning %v\n", err, target.Address.String())
}
if result != nil && err == nil {
return result, nil
}
}
}
// TCP TLS 类型的....
// 快速模式 也就是只检查默认端口
if c.FastMode {
return nil, nil
}
if isTLS {
// ...
} else {
// 按照优先级遍历所有插件进行指纹识别 这里感觉可以去掉上面的默认服务的识别 可以简单增加一个判断
for _, plugin := range sortedTCPPlugins {
conn, err := DialTCP(ip, port)
if err != nil {
return nil, fmt.Errorf("unable to connect, err = %w", err)
}
result, err := simplePluginRunner(conn, target, c, plugin)
if err != nil && c.Verbose {
log.Printf("error: %v scanning %v\n", err, target.Address.String())
}
if result != nil && err == nil {
// identified plugin match
return result, nil
}
}
}
return nil, nil
}

这里是遍历了 2 次,第一次是获取默认服务,第二次是去按照优先级全部扫一遍。这里感觉有些麻烦,可以向 go-Portscan 那样维护一个端口和默认服务的 map,这里就维护一个端口和插件的 map ,就不需要一直遍历获取默认服务插件了。

之后就是 simplePluginRunner 了,它其实就是调用插件的 Run 方法去识别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func simplePluginRunner(
conn net.Conn,
target plugins.Target,
config *Config,
plugin plugins.Plugin,
) (*plugins.Service, error) {
if config.Verbose {
log.Printf("%v %v-> scanning %v\n",
target.Address.String(),
target.Host,
plugins.CreatePluginID(plugin),
)
}
result, err := plugin.Run(conn, config.DefaultTimeout, target)
if config.Verbose {
log.Printf(
"%v %v-> completed %v\n",
target.Address.String(),
target.Host,
plugins.CreatePluginID(plugin),
)
}
return result, err
}

学习总结

  • fingerprintx 接口 插件,然后通过 import 导包去实现插件注册挺不错的

fingerprintx 源码学习
https://liancccc.github.io/2024/02/20/技术/源码学习/fingerprintx/
作者
守心
发布于
2024年2月20日
许可协议