Ehole 指纹探测工具源码分析
一、项目简介
EHole 是一款域名资产系统指纹识别的工具。
github 地址 https://github.com/EdgeSecurityTeam/EHole
二、项目结构
项目基于 cobra 命令行框架进行编写,程序执行指令和参数配置在 cmd 目录下。
├── cmd
│ ├── finger.go # finger 命令行参数配置
│ ├── fofaext.go # fofaext 命令行参数配置
│ └── root.go # 命令行入口
├── config.ini # fofa 配置文件
├── finger.json # 指纹库
├── go.mod
├── go.sum
├── images
│ ├── 16106897804249.jpg
│ └── 16106898229421.jpg
├── LICENSE
├── main.go # 主程序入口
├── module
│ ├── finger
│ │ ├── encoding.go # 编码转换
│ │ ├── faviconhash.go # 计算站点 favicon.ico 的 hash 值
│ │ ├── finger.go # 创建执行扫描任务
│ │ ├── getfingerfile.go # 加载指纹库
│ │ ├── http.go # http 工具函数。获取站点标题、favicon.ico hash、http 请求
│ │ ├── jsjump.go # 提取 js 代码里的跳转 url
│ │ ├── matchfinger.go # 指纹匹配函数
│ │ ├── output.go # 格式化输出函数
│ │ └── source
│ │ ├── fofa.go # fofa 检索函数
│ │ └── localfile.go # 本地文件读取函数
│ ├── fofaext
│ │ └── fofaext.go # 存储 fofa 查询到的资产
│ └── queue
│ ├── queue.go # 队列
│ └── queue_test.go
└── README.md
三、具体功能
3.1 指纹库
指纹库文件为 json 格式,参数包括 cms 名称、method 匹配方式、location 匹配位置、keyword 关键字组成。其中 method 分为两种,第一种是 keyword ,从网络请求中匹配特征值,location 位置参数可选 title 标题、header 请求头以及 body 请求体。
{
"cms": "Kibana",
"method": "keyword",
"location": "title",
"keyword": ["Kibana"]
}, {
"cms": "Swagger UI",
"method": "keyword",
"location": "body",
"keyword": ["Swagger UI"]
}, {
"cms": "ThinkPHP",
"method": "keyword",
"location": "header",
"keyword": ["ThinkPHP"]
},
第二种是 faviconhash,通过匹配 favicon.ico 的 hash 值进行 cms 识别,location 位置参数填写 body。
{
"cms": "Jenkins",
"method": "faviconhash",
"location": "body",
"keyword": ["81586312"]
}
*FinScan.fingerScan 方法中有提供 regular 正则的匹配方式,但指纹库中未提供对应规则。
3.2 创建扫描任务
当我们输入 finger 命令执行指纹识别扫描任务时,都会使用到 NewScan 函数、StartScan 方法。
var fingerCmd = &cobra.Command{
...
s := finger.NewScan(urls, thread, output,proxy)
s.StartScan()
...
}
跟进 NewScan,这里创建了 FinScan 类型对象,参数包括
(1)UrlQueue 用于存储待扫描的 url
(2)Ch 通道,缓冲区大小与线程数一致
(3)Wg 用于等待所有扫描任务完成
(4)Thread 用于控制扫描的线程数,默认值是100
(5)Output 用于设置扫描结果输出路径及格式
(6)Proxy 用于设置网络代理
(7)AllResult 是所有资产扫描结果的切片
(8)FocusResult 是基于指纹库能识别到的重点资产扫描结果的切片
后续调用 LoadWebfingerprint 加载指纹库,再通过 GetWebfingerprint 加载指纹信息,最后将 url 插入队列。
func NewScan(urls []string, thread int, output string, proxy string) *FinScan {
s := &FinScan{
UrlQueue: queue.NewQueue(),
Ch: make(chan []string, thread),
Wg: sync.WaitGroup{},
Thread: thread,
Output: output,
Proxy: proxy,
AllResult: []Outrestul{},
FocusResult: []Outrestul{},
}
err := LoadWebfingerprint(source.GetCurrentAbPathByExecutable() + "/finger.json")
if err != nil {
color.RGBStyleFromString("237,64,35").Println("[error] fingerprint file error!!!")
os.Exit(1)
}
s.Finpx = GetWebfingerprint()
for _, url := range urls {
s.UrlQueue.Push([]string{url,"0"})
}
return s
}
StartScan 主要是起多线程扫描任务,扫描由 fingerScan 执行。
func (s *FinScan)StartScan() {
for i := 0; i <= s.Thread; i++ {
s.Wg.Add(1)
go func() {
defer s.Wg.Done()
s.fingerScan()
}()
}
s.Wg.Wait()
...
}
3.3 获取站点指纹
想要识别站点系统,首先肯定需要获取站点指纹特征。
(1)获取站点 title
func gettitle(httpbody string) string {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(httpbody))
if err != nil {
return "Not found"
}
title := doc.Find("title").Text()
title = strings.Replace(title, "\n", "", -1)
title = strings.Trim(title, " ")
return title
}
(2)获取网络请求 header
同时从 header 提取 Server 和 X-Powered-By 作为 server 信息。
resp, err := client.Do(req)
httpheader := resp.Header
var server string
capital, ok := httpheader["Server"]
if ok {
server = capital[0]
} else {
Powered, ok := httpheader["X-Powered-By"]
if ok {
server = Powered[0]
} else {
server = "None"
}
}
(3)获取网络请求 body
调用 toUtf8 函数对 body 进行编码转换
resp, err := client.Do(req)
result, _ := ioutil.ReadAll(resp.Body)
httpbody := string(result)
httpbody = toUtf8(httpbody, contentType)
(4)获取站点 favicon.ico 并计算 hash
getfavicon 函数用于获取 favicon.ico。先通过 js 代码正则检索是否有 favicon 文件,若存在,则将域名与 favicon 文件拼接作为站点 favicon 访问路径,若不存在,则站点 favicon 访问路径为 http://host/favicon.ico。
func getfavicon(httpbody string, turl string) string {
faviconpaths := xegexpjs(`href="(.*?favicon....)"`, httpbody)
var faviconpath string
u, err := url.Parse(turl)
if err != nil {
panic(err)
}
turl = u.Scheme + "://" + u.Host
if len(faviconpaths) > 0 {
fav := faviconpaths[0][1]
if fav[:2] == "//" {
faviconpath = "http:" + fav
} else {
if fav[:4] == "http" {
faviconpath = fav
} else {
faviconpath = turl + "/" + fav
}
}
} else {
faviconpath = turl + "/favicon.ico"
}
return favicohash(faviconpath)
}
后续到 favicohash 里计算 favicon.ico 的 hash 值。成功读取到 favicon.ico 文件后,先进行 base64 编码,需要注意的是每76个字符后需要插入一次换行符,处理完毕后结尾再插入一次换行符。
// module/finger/faviconhash.go:27
func StandBase64(braw []byte) []byte {
bckd := base64.StdEncoding.EncodeToString(braw)
var buffer bytes.Buffer
for i := 0; i < len(bckd); i++ {
ch := bckd[i]
buffer.WriteByte(ch)
if (i+1)%76 == 0 {
buffer.WriteByte('\n')
}
}
buffer.WriteByte('\n')
return buffer.Bytes()
}
再调用 Mmh3Hash32 进行 Hash 运算,得到最终的 favicon.ico hash 值。
import (
"github.com/twmb/murmur3"
)
func Mmh3Hash32(raw []byte) string {
var h32 hash.Hash32 = murmur3.New32()
_, err := h32.Write([]byte(raw))
if err == nil {
return fmt.Sprintf("%d", int32(h32.Sum32()))
} else {
return "0"
}
}
3.4 匹配指纹特征
匹配指纹目前由 iskeyword 执行,主要就是使用 strings.Contains 进行检测,查看从站点获取到的指纹特征是否与指纹库中的 keyword 完全匹配。
// module/finger/finger.go:95
func (s *FinScan)fingerScan() {
...
for _, finp := range s.Finpx.Fingerprint {
if finp.Location == "body" {
if finp.Method == "keyword" {
if iskeyword(data.body, finp.Keyword) {
cms = append(cms, finp.Cms)
}
...
}
// module/finger/matchfinger.go:8
func iskeyword(str string, keyword []string) bool {
var x bool
x = true
for _, k := range keyword {
if strings.Contains(str, k) {
x = x && true
} else {
x = x && false
}
}
return x
}
四、总结
Web 站点指纹识别的工具很多,例如 WhatWeb、TideFinger等,原理大同小异,只是实现的方式不一样,重要的还是指纹规则库的积累。