本次主要分析指纹识别部分,针对开放http的端口的服务信息进行识别。
项目来源:https://github.com/XinRoom/go-portScan/blob/main/util/file.go
此文件的功能,主要是从html页面中提取favicon的url,然后计算favicon的hash值
此文件中主要包含三个方法:
从 HTML 页面的字符串中提取 favicon 的 URL;使用正则表达式 shortcutText
匹配包含 favicon 的 <link>
标签,然后再使用 shortcutHref
提取其 href
属性中的 URL
计算给定字节切片的 MurmurHash3 32 位哈希值;使用 MurmurHash3 算法对输入的字节切片进行哈希运算,返回结果作为字符串表示。
对二进制数据进行标准的 Base64 编码,并按行分割。将输入的二进制数据使用标准的 Base64 编码成字符串,然后按照每行字符数为 76 的标准,将编码后的结果分割成多行。
package webfinger
import (
"bytes"
"encoding/base64"
"fmt"
"github.com/twmb/murmur3"
"regexp"
)
var (
shortcutText = regexp.MustCompile(`(?im)<link.*?rel=["']?shortcut icon["']?.*?>`)//提取link标签的正则表达式
shortcutHref = regexp.MustCompile(`(?im)href=['"]+(.*?)['"]+`)//提取link标签里的href值的正则表达式
)
func FindFaviconUrl(body string) string {
a := shortcutText.FindStringSubmatch(body)//首先根据正则提取传入的html页面的link标签,赋值给a
if len(a) > 0 {//如果存在link标签
faviconLink := a[0]
b := shortcutHref.FindStringSubmatch(faviconLink)//提取第一个link标签的href的值
if len(b) > 1 {
return b[1] //如果提取url成功,则返回url
}
}
return ""//不存在link标签,返回空
}
func mmh3Hash32(raw []byte) string {
var h32 = murmur3.New32()
_, err := h32.Write(raw)
if err == nil {
return fmt.Sprintf("%d", int32(h32.Sum32()))//使用 MurmurHash3 算法对输入的字节切片进行哈希运算,返回结果作为字符串表示。
} else {
return ""
}
}
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')//将输入的二进制数据使用标准的 Base64 编码成字符串,然后按照每行字符数为 76 的标准,将编码后的结果分割成多行。
}
}
buffer.WriteByte('\n')
return buffer.Bytes()
}
这个文件主要为faviconhash的字典,用来通过faviconhash的值比对来获取指纹信息。
此文件代码是一个 Web 系统指纹识别器。它的主要目的是从 HTTP 响应或 Favicon.ico 数据中识别 Web 服务的指纹。下面是一步步的解释:
Date
结构体表示特定指纹识别方法的数据结构;
WebFinger
结构体用于保存不同的指纹集合
type Date struct {
Name string //指纹名称
Location string //在http响应中的位置
Method string //指纹识别方法(如keyword、regular、faviconhash)
Keyword []string //关键字或者正则表达式数组
}
type WebFinger struct {
Name string //指纹集合的名称
Fingers []Date //包含data结构体的切片
}
var WebFingers []WebFinger
//go:embed finger.json
var DefFingerData []byte
解析 Web 指纹数据。
func ParseWebFingerData(data []byte) error {
err := json.Unmarshal(data, &WebFingers) //将json数据保存到WebFingers实例中
if err != nil {
return err
}
return nil
}
从文件中加载 Web 指纹数据。
//LoadWebFingerData 加载web指纹数据
func LoadWebFingerData(file string) error {
data, err := os.ReadFile(file)
if err != nil {
return err
}
err = ParseWebFingerData(data)
if err != nil {
return err
}
return nil
}
此函数解析 HTTP 响应的内容和头部信息,然后遍历 WebFingers
中的指纹集合,检查是否匹配给定的指纹识别方法和关键字或正则表达式;如果匹配,则返回匹配的指纹名称。?
- ?解析 HTTP 响应的内容和头部信息。
- 遍历
WebFingers
中的指纹集合,检查是否匹配给定的指纹识别方法和关键字或正则表达式。- 如果匹配,则返回匹配的指纹名称。
//WebFingerIdent web系统指纹识别 用指纹库的指纹循环比对response中header部分和body部分是否包含特征
func WebFingerIdent(resp *http.Response) (names []string){
var dataMap = make(map[string]string)
body, _ := io.ReadAll(resp.Body)
dataMap["body"] = string(body)
var b bytes.Buffer
resp.Header.Write(&b)
dataMap["header"] = b.String()
for _,finger := range WebFingers {
for _, finger2 := range finger.Fingers {
var flag bool
if _, ok := dataMap[finger2.Location]; !ok {//取指纹库中Location字段为body和header部分的指纹
continue
}
switch finger2.Method {
case "keyword" :
if iskeyword(dataMap[finger2.Location], finger2.Keyword){
flag = true
}
case "regular" :
if isregular(dataMap[finger2.Location], finger2.Keyword){
flag = true
}
}
if flag {
if finger2.Name != ""{
finger.Name += "," + finger2.Name
}
names = append(names, finger.Name)
break
}
}
}
return
}
?此函数通过 Favicon.ico 识别 Web 系统的指纹。首先对对 Favicon.ico 数据进行哈希运算,然后遍历WebFingers
中的指纹集合,检查是否与哈希值匹配指定的指纹识别方法和关键字。如果匹配,则返回匹配的指纹名称。
- 对 Favicon.ico 数据进行哈希运算。
- 遍历
WebFingers
中的指纹集合,检查是否与哈希值匹配指定的指纹识别方法和关键字。- 如果匹配,则返回匹配的指纹名称。
// WebFingerIdentByFavicon web系统指纹识别,通过Favicon.ico用指纹库的指纹循环比对favico hash之判断是否包含特征
func WebFingerIdentByFavicon(body []byte) (names []string) {
var data string
data = mmh3Hash32(standBase64(body))
for _, finger := range WebFingers {
for _, finger2 := range finger.Fingers {
switch finger2.Method {
case "faviconhash":
if data != "" && len(finger2.Keyword) > 0 && data == finger2.Keyword[0] {
if finger2.Name != "" {
finger.Name += "," + finger2.Name
}
names = append(names, finger.Name)
break
}
}
}
}
return
}
此文件包含两个方法,iskeyword和isregular,都是用于检查字符串中是否存在特定的关键字或匹配正则表达式
检查字符串中是否存在指定的关键字。遍历关键字数组,如果字符串中不包含其中任何一个关键字,则立即返回 false
,否则返回 true
。
检查字符串是否匹配指定的正则表达式。遍历正则表达式数组,使用 regexp.MustCompile
创建正则表达式对象,然后检查字符串是否与每个正则表达式匹配。如果字符串不匹配任何一个正则表达式,则立即返回 false
,否则返回 true
。
func iskeyword(str string, keyword []string) bool {
if len(keyword) == 0 || str == "" {//先判断传入关键字切片是否为空和字符串是否为空
return false
}
//遍历关键字数组,如果字符串中不包含其中任何一个关键字,则立即返回
for _, k := range keyword {//循环遍历keyword切片,检查keyword是否都包含在str字符串中
if !strings.Contains(str, k) {
return false
}
}
return true
}
//遍历正则表达式数组,使用 regexp.MustCompile 创建正则表达式对象,然后检查字符串是否与每个正则表达式匹配。如果字符串不匹配任何一个正则表达式,则立即返回 false,否则返回 true。
func isregular(str string, keyword []string) bool {
if len(keyword) == 0 || str == "" {
return false
}
for _, k := range keyword {
re := regexp.MustCompile(k)
if !re.Match([]byte(str)) {
return false
}
}
return true
}
mmh3Hash32(raw []byte) string
:对数据进行哈希并返回哈希值的字符串表示形式。standBase64(braw []byte) []byte
:对数据进行 Base64 编码,并在每行末尾添加换行符。此文件主要是一些用于字符编码转换的函数,以及一个用于从http响应中提取标题并根据响应的内容类型进行相应的解码的函数。
这个函数用于将 GBK 编码的字节序列转换为 UTF-8 编码的字节序列。它使用
simplifiedchinese.GBK.NewDecoder()
进行解码,将输入的 GBK 编码字节流转换为 UTF-8 编码//将GBK编码转化UTF-编码 func Decodegbk(s []byte) ([]byte, error){ I := bytes.NewReader(s) O := transform.NewReader(I, simplifiedchinese.GBK.NewDecoder()) d, e := ioutil.ReadAll(O) if e != nil { return nil, e } return d, nil }
类似于
Decodegbk
,这个函数将 BIG5 编码的字节序列转换为 UTF-8 编码的字节序列,使用traditionalchinese.Big5.NewDecoder()
进行解码。func Decodebig5(s []byte) ([]byte, error){ I := bytes.NewReader(s) O := transform.NewReader(I, traditionalchinese.Big5.NewDecoder()) d, e := ioutil.ReadAll(O) if e != nil { return nil, e } return d, nil }
这个函数是将 UTF-8 编码的字节序列转换为 BIG5 编码的字节序列,使用
traditionalchinese.Big5.NewEncoder()
进行编码。func Encodebig5(s []byte) ([]byte, error){ I := bytes.NewReader(s) O := transform.NewReader(I, traditionalchinese.Big5.NewEncoder()) d, e := ioutil.ReadAll(O) if e != nil { return nil, e } return d,nil }
这个函数是用于将韩文编码的字节序列转换为 UTF-8 编码的字节序列,使用
korean.EUCKR.NewDecoder()
进行解码。func DecodeKorean(s []byte) ([]byte, error) { koreanDecoder := korean.EUCKR.NewDecoder() return koreanDecoder.Bytes(s) }
这个函数从 HTTP 响应中提取数据,并根据响应的内容类型进行相应的解码。它会检查响应头中的 Content-Type
,尝试根据特定的字符集(如 GBK、EUC-KR 等)对数据进行解码。如果检测到响应头中指定了字符集,会调用相应的解码函数进行转换,如果没有匹配到特定的字符集,就会尝试从 HTML 头部的 meta 标签中提取字符集信息,并根据提取的信息进行解码。最终返回解码后的数据或原始数据(如果未指定字符集或解码失败)。
func DecodeData(data []byte, headers http.Header) ([]byte, error) { //Non UTF-8 if contentTypes, ok := headers["Content-Type"]; ok { contentType := strings.ToLower(strings.Join(contentTypes, ";"))//用;连接contenttypes内容,并转化为小写 //根据不同的contentyps值调用不同的解密方法 switch { case stringsutil.ContainsAny(contentType, "charset=gb2312","charset=gbk"): return Decodegbk([]byte(data)) case stringsutil.ContainsAyn(contentType,"euc-kr"): return DecodeDorean(data) } //Content-Type 来自 head tag var match = reContentType.FindSubmatch(data)//reContentType来自 core/port/fingerprint/title.go ,作用为regexp.MustCompile(`(?im)\s*charset="(.*?)"|charset=(.*?)"\s*`),根据正则提取对应字段 var mcontentType = "" if len(match) != 0 { for i, v := range match{ if string(v) != "" && i != 0 { mcontentType = string(v) } } mcontentType = strings.ToLower(mcontentType) } switch { case stringsutil.ContainsAny(mcontentType, "gb2312", "gbk"): return Decodegbk(data) } } return data, nil }
ps:这些函数的作用在于根据不同的字符编码格式,将原始的字符序列转换为 UTF-8 编码的字符序列,使其能够被正确解析和处理。DecodeData
函数则是根据 HTTP 响应的内容类型来判断使用何种字符编码转换函数,以确保正确地解码数据
这个文件主要用于端口服务的指纹识别,主要功能如下:
首先创建常量和类型:
type Action uint8
const (
ActionRecv = Action(iota)
ActionSend
)
const (
refusedStr = "refused"
ioTimeoutStr = "i/o timeout"
)
type ruleData struct {
Action Action // send or recv
Data []byte // send or match data
Regexps []*regexp.Regexp
}
type serviceRule struct {
Tls bool
DataGroup []ruleData
}
var serviceRules = make(map[string]serviceRule)
var readBufPool = &sync.Pool{
New: func() interface{} {
return make([]byte, 4096)
},
}
函数如下:
此函数识别端口对应的服务。它按照指定的顺序尝试从一系列服务规则中匹配出对应的服务。如果连接失败或者无法匹配出服务,则返回"unknown"
// PortIdentify 端口识别 func PortIdentify(network string, ip net.IP, _port uint16, dailTimeout time.Duration) (serviceName string, isDailErr bool) { matchedRule := make(map[string]struct{}) unknown := "unknown" var matchStatus int // 优先判断port可能的服务 if serviceNames, ok := portServiceOrder[_port]; ok {//portServiceOrder为rule.go文件中常见端口服务对应列表 for _, service := range serviceNames { matchedRule[service] = struct{}{} matchStatus = matchRule(network, ip, _port, serviceRules[service], dailTimeout)//获取服务端口开放状态 if matchStatus == 1 { return service, false } else if matchStatus == -1 { return unknown, true } } } // onlyRecv { var conn net.Conn var n int buf := readBufPool.Get().([]byte) defer func() { readBufPool.Put(buf) }() address := fmt.Sprintf("%s:%d", ip, _port) conn, _ = net.DialTimeout(network, address, dailTimeout) if conn == nil { return unknown, true } n, _ = read(conn, buf) conn.Close() if n != 0 { for _, service := range onlyRecv { _, ok := matchedRule[service] //检测是不是已有服务 if ok { continue } matchStatus = matchRuleWhithBuf(buf[:n], ip, _port, serviceRules[service])//判断端口服务开放状态 if matchStatus == 1 { return service, false } } } for _, service := range onlyRecv { matchedRule[service] = struct{}{} } } // 优先判断Top服务 for _, service := range serviceOrder { _, ok := matchedRule[service] if ok { continue } matchedRule[service] = struct{}{} matchStatus = matchRule(network, ip, _port, serviceRules[service], dailTimeout) if matchStatus == 1 { return service, false } else if matchStatus == -1 { return unknown, true } } // other for service, rule := range serviceRules { _, ok := matchedRule[service] if ok { continue } matchStatus = matchRule(network, ip, _port, rule, dailTimeout) if matchStatus == 1 { return service, false } else if matchStatus == -1 { return unknown, true } } return unknown, false }
函数根据传入的数据缓冲区,匹配给定的服务规则。如果规则中的数据在缓冲区中找到了匹配,则返回成功状态。
// 指纹匹配函数 func matchRuleWhithBuf(buf, ip net.IP, _port uint16, serviceRule serviceRule) int { data := []byte("") // 逐个判断 for _, rule := range serviceRule.DataGroup { if rule.Data != nil {//先替换请求中的数据data数据,比如IP和port data = bytes.Replace(rule.Data, []byte("{IP}"), []byte(ip.String()), -1) data = bytes.Replace(data, []byte("{PORT}"), []byte(strconv.Itoa(int(_port))), -1) } // 包含数据就正确,使用正则匹配 if rule.Regexps != nil { for _, _regex := range rule.Regexps { if _regex.MatchString(convert2utf8(string(buf))) { return 1 } } } if bytes.Compare(data, []byte("")) != 0 && bytes.Contains(buf, data) { return 1 } } return 0 }
此函数建立连接并根据连接的情况进行数据的发送和接收,然后匹配给定的服务规则。如果规则中的数据在接收的数据中找到了匹配,则返回成功状态。
// 指纹匹配函数 func matchRule(network string, ip net.IP, _port uint16, serviceRule serviceRule, dailTimeout time.Duration) int { var err error var isTls bool var conn net.Conn var connTls *tls.Conn address := fmt.Sprintf("%s:%d", ip, _port) // 建立连接 if serviceRule.Tls {//先确定是不是https // tls connTls, err = tls.DialWithDialer(&net.Dialer{Timeout: dailTimeout}, network, address, &tls.Config{ InsecureSkipVerify: true, MinVersion: tls.VersionTLS10, }) if err != nil { if strings.HasSuffix(err.Error(), ioTimeoutStr) || strings.Contains(err.Error(), refusedStr) { return -1 } return 0 } defer connTls.Close() isTls = true } else {//如果不是https,使用net.conn连接 conn, err = net.DialTimeout(network, address, dailTimeout) if conn == nil { return -1 } defer conn.Close() } buf := readBufPool.Get().([]byte) defer func() { readBufPool.Put(buf) }() data := []byte("") // 逐个判断 for _, rule := range serviceRule.DataGroup { if rule.Data != nil {//替换所有data数据中的IP和port,针对http和https data = bytes.Replace(rule.Data, []byte("{IP}"), []byte(ip.String()), -1) data = bytes.Replace(data, []byte("{PORT}"), []byte(strconv.Itoa(int(_port))), -1) } if rule.Action == ActionSend {//先判断获取发送数据,进行conn连接 if isTls { connTls.SetWriteDeadline(time.Now().Add(time.Second)) _, err = connTls.Write(data) } else { conn.SetWriteDeadline(time.Now().Add(time.Second)) _, err = conn.Write(data) } if err != nil { // 出错就退出 return 0 } } else {//针对recive进行数据判断 var n int if isTls { n, err = read(connTls, buf) } else { n, err = read(conn, buf) } // 出错就退出 if n == 0 { return 0 } // 包含数据就正确 使用正则筛选返回包数据 if rule.Regexps != nil { for _, _regex := range rule.Regexps { if _regex.MatchString(convert2utf8(string(buf[:n]))) { return 1 } } } //判断数据是否为空,以及buf中是否包含数据 if bytes.Compare(data, []byte("")) != 0 && bytes.Contains(buf[:n], data) { return 1 } } } return 0 }
函数负责从连接中读取数据,并在指定时间内设置读取的截止时间。它会尝试从传入的连接中读取数据到缓冲区中,并返回读取的字节数和可能出现的错误。如果读取的数据长度为 0 或者发生错误,则会返回相应的状态信息。
func read(conn interface{}, buf []byte) (int, error) { switch conn.(type) { case net.Conn: conn.(net.Conn).SetReadDeadline(time.Now().Add(time.Second)) return conn.(net.Conn).Read(buf[:]) case *tls.Conn: conn.(*tls.Conn).SetReadDeadline(time.Now().Add(time.Second)) return conn.(*tls.Conn).Read(buf[:]) } return 0, errors.New("unknown type") }
函数用于修复正则表达式在匹配非UTF-8字符时可能出现的问题。它会将UTF-8以外的字符转换成对应的UTF-8字符,确保正则表达式可以正确匹配。
func convert2utf8(src string) string { var dst string for i, r := range src { var v string if r == utf8.RuneError { // convert, rune => string, intstring() => encoderune() v = string(src[i]) } else { v = string(r) } dst += v } return dst }
?ps:代码中利用 sync.Pool
作为缓冲池,用来存储读取数据的缓冲区,提高效率。总体来说,这些函数实现了通过发送和接收数据并根据特定规则匹配服务的功能,用于识别特定端口上可能运行的服务
这个文件主要为一个指纹识别器,它尝试根据不同服务的特征来判断特定端口上的服务。这里的 serviceOrder
是服务的顺序列表,onlyRecv
是仅接收数据的服务列表,portServiceOrder
是特定端口和服务的映射关系。
var serviceOrder = []string{"http", "https", "ssh", "redis", "mysql"}
var onlyRecv []string
var portServiceOrder = map[uint16][]string{
21: {"ftp"},
22: {"ssh"},
80: {"http", "https"},
443: {"https", "http"},
445: {"smb"},
1035: {"oracle"},
1080: {"socks5", "socks4"},
1081: {"socks5", "socks4"},
1082: {"socks5", "socks4"},
1083: {"socks5", "socks4"},
1433: {"sqlserver"},
1521: {"oracle"},
1522: {"oracle"},
1525: {"oracle"},
1526: {"oracle"},
1574: {"oracle"},
1748: {"oracle"},
1754: {"oracle"},
3306: {"mysql"},
3389: {"ms-wbt-server"},
6379: {"redis"},
9001: {"mongodb"},
11211: {"memcached"},
14238: {"oracle"},
27017: {"mongodb"},
20000: {"oracle"},
49153: {"mongodb"},
}
init()
函数中,为了不同的服务(如HTTP、HTTPS、SSH等),定义了对应的识别规则。每个服务都有其特定的数据发送和接收规则,用于匹配返回的数据以确定服务是否存在。例如,对于 HTTP 服务,它尝试发送一个 HTTP 头部,并尝试匹配返回数据中是否包含HTTP/
;对于 SSH 服务,它会尝试匹配返回数据中是否包含 SSH 相关的标识等。这些规则都是通过发送特定数据和匹配接收到的数据来进行识别的func init() { // http serviceRules["http"] = serviceRule{ //pingerprint.go中结构体,包含Tls和DataGroup Tls: false, DataGroup: []ruleData{ {//设置http和https的规则数据,包含请求头和接收包 ActionSend, []byte("HEAD / HTTP/1.1\r\nHost: {IP}\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0\r\nAccept: */*\r\nAccept-Language: en\r\nAccept-Encoding: deflate\r\n\r\n"), nil, }, { ActionRecv, []byte("HTTP/"), nil, }, }, } // https serviceRules["https"] = serviceRule{ Tls: true, DataGroup: serviceRules["http"].DataGroup, } // ssh serviceRules["ssh"] = serviceRule{ Tls: false, DataGroup: []ruleData{ {//设置ssh的匹配规则 ActionRecv, nil, []*regexp.Regexp{ regexp.MustCompile(`^SSH-([\d.]+)-`), regexp.MustCompile(`^SSH-(\d[\d.]+)-`), regexp.MustCompile(`^SSH-(\d[\d.]*)-`), regexp.MustCompile(`^SSH-2\.0-`), regexp.MustCompile(`^SSH-1\.`), }, }, }, } // ftp serviceRules["ftp"] = serviceRule{ Tls: false, DataGroup: []ruleData{ {//设置ftp的匹配规则 ActionRecv, nil, []*regexp.Regexp{ regexp.MustCompile(`^220 ([-/.+\w]+) FTP server`), regexp.MustCompile(`^220[ |-](.*?)FileZilla`), regexp.MustCompile(`^(?i)220[ |-](.*?)version`), regexp.MustCompile(`^220 3Com `), regexp.MustCompile(`^220-GuildFTPd`), regexp.MustCompile(`^220-.*\r\n220`), regexp.MustCompile(`^220 Internet Rex`), regexp.MustCompile(`^530 Connection refused,`), regexp.MustCompile(`^220 IIS ([\w._-]+) FTP`), regexp.MustCompile(`^220 PizzaSwitch `), regexp.MustCompile(`(?i)^220 ([-.+\w]+) FTP`), regexp.MustCompile(`(?i)^220[ |-](.*?)FTP`), }, }, }, } // socks4 serviceRules["socks4"] = serviceRule{ Tls: false, DataGroup: []ruleData{ {//设置socks4发送包的数据部分 ActionSend, []byte("\x04\x01\x00\x16\x7f\x00\x00\x01rooo\x00"), nil, }, {//设置socks4接收包的匹配规则 ActionRecv, nil, []*regexp.Regexp{ regexp.MustCompile(`^\x00\x5a`), regexp.MustCompile(`^\x00\x5b`), regexp.MustCompile(`^\x00\x5c`), regexp.MustCompile(`^\x00\x5d`), }, }, }, } // socks5 serviceRules["socks5"] = serviceRule{ Tls: false, DataGroup: []ruleData{ {//设置socks5的发送包数据 ActionSend, []byte("\x05\x04\x00\x01\x02\x80\x05\x01\x00\x03\x0dwww.baidu.com\x00\x50GET / HTTP/1.0\r\n\r\n"), nil, }, {//设置socks5的接收包规则 ActionRecv, nil, []*regexp.Regexp{ regexp.MustCompile(`^\x05\x00\x05\x01`), regexp.MustCompile(`^\x05\x00\x05\x00\x00\x01.{6}HTTP`), regexp.MustCompile(`^\x05\x02`), regexp.MustCompile(`^\x05\x00`), }, }, }, } tls //serviceRules["tls"] = serviceRule{ // Tls: false, // DataGroup: []ruleData{ // { // ActionSend, // []byte("\x16\x03\x00\x00S\x01\x00\x00O\x03\x00?G\xd7\xf7\xba,\xee\xea\xb2`~\xf3\x00\xfd\x82{\xb9\xd5\x96\xc8w\x9b\xe6\xc4\xdb<=\xdbo\xef\x10n\x00\x00(\x00\x16\x00\x13\x00\x0a\x00f\x00\x05\x00\x04\x00e\x00d\x00c\x00b\x00a\x00`\x00\x15\x00\x12\x00\x09\x00\x14\x00\x11\x00\x08\x00\x06\x00\x03\x01\x00"), // nil, // }, // { // ActionRecv, // nil, // []*regexp.Regexp{ // regexp.MustCompile(`^[\x16\x15]\x03\x00`), // regexp.MustCompile(`^[\x16\x15]\x03...\x02`), // }, // }, // }, //} // smb serviceRules["smb"] = serviceRule{ Tls: false, DataGroup: []ruleData{ {//设置smb协议的发送包数据 ActionSend, []byte("\x00\x00\x00\xa4\xff\x53\x4d\x42\x72\x00\x00\x00\x00\x08\x01\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x06\x00\x00\x01\x00\x00\x81\x00\x02PC NETWORK PROGRAM 1.0\x00\x02MICROSOFT NETWORKS 1.03\x00\x02MICROSOFT NETWORKS 3.0\x00\x02LANMAN1.0\x00\x02LM1.2X002\x00\x02Samba\x00\x02NT LM 0.12\x00\x02NT LANMAN 1.0\x00"), nil, }, {//设置smb协议的接收包规则 ActionRecv, nil, []*regexp.Regexp{ regexp.MustCompile(`MBr\x00\x00\x00\x00\x88\x01@\x00`), }, }, }, } // ms-wbt-server serviceRules["ms-wbt-server"] = serviceRule{ Tls: false, DataGroup: []ruleData{ { ActionSend, []byte("\x03\x00\x00*%\xe0\x00\x00\x00\x00\x00Cookie: mstshash=pcpc\r\n\x01\x00\x08\x00\x03\x00\x00\x00"), nil, }, { ActionRecv, nil, []*regexp.Regexp{ regexp.MustCompile(`\x03\x00\x00.\x0e\xd0\x00\x00\x124\x00`), }, }, }, } // jdwp serviceRules["jdwp"] = serviceRule{ Tls: false, DataGroup: []ruleData{ { ActionRecv, []byte("JDWP-Handshake"), nil, }, }, } // jdbc serviceRules["jdbc"] = serviceRule{ Tls: false, DataGroup: []ruleData{ { ActionRecv, []byte("HSQLDB JDBC Network Listener"), nil, }, }, } // Db // mysql serviceRules["mysql"] = serviceRule{ Tls: false, DataGroup: []ruleData{ { ActionRecv, nil, []*regexp.Regexp{ regexp.MustCompile(`(?s)^.\x00\x00\x00\xff..Host .* is not allowed to connect to this .* server$`), regexp.MustCompile(`^.\x00\x00\x00\xff..Too many connections`), regexp.MustCompile(`(?s)^.\x00\x00\x00\xff..Host .* is blocked because of many connection errors`), regexp.MustCompile(`(?s)^.\x00\x00\x00\x0a(\d\.[-_~.+:\w]+MariaDB-[-_~.+:\w]+)`), regexp.MustCompile(`(?s)^.\x00\x00\x00\x0a(\d\.[-_~.+\w]+)\x00`), regexp.MustCompile(`(?s)^.\x00\x00\x00\xffj\x04'[\d.]+' .* MySQL`), }, }, }, } // redis serviceRules["redis"] = serviceRule{ Tls: false, DataGroup: []ruleData{ { ActionSend, []byte("GET / HTTP/1.1\r\n"), nil, }, { ActionRecv, nil, []*regexp.Regexp{ regexp.MustCompile(`-ERR operation not permitted\r\n`), regexp.MustCompile(`-ERR wrong number of arguments for 'get' command\r\n`), }, }, }, } // sqlserver serviceRules["sqlserver"] = serviceRule{ Tls: false, DataGroup: []ruleData{ { ActionSend, []byte("\x12\x01\x00\x34\x00\x00\x00\x00\x00\x00\x15\x00\x06\x01\x00\x1b\x00\x01\x02\x00\x1c\x00\x0c\x03\x00\x28\x00\x04\xff\x08\x00\x01\x55\x00\x00\x00\x4d\x53\x53\x51\x4c\x53\x65\x72\x76\x65\x72\x00\x48\x0f\x00\x00"), nil, }, { ActionRecv, []byte("\x04\x01\x00\x25\x00\x00\x01\x00\x00\x00\x15\x00\x06\x01\x00\x1b\x00\x01\x02\x00\x1c\x00\x01\x03\x00\x1d\x00\x00\xff"), nil, }, }, } // oracle serviceRules["oracle"] = serviceRule{ Tls: false, DataGroup: []ruleData{ { ActionSend, []byte("\x00Z\x00\x00\x01\x00\x00\x00\x016\x01,\x00\x00\x08\x00\x7F\xFF\x7F\x08\x00\x00\x00\x01\x00 \x00:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x004\xE6\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00(CONNECT_DATA=(COMMAND=version))"), nil, }, { ActionRecv, nil, []*regexp.Regexp{ regexp.MustCompile(`(?s)^\x00\x20\x00\x00\x02\x00\x00\x00\x016\x00\x00\x08\x00\x7f\xff\x01\x00\x00\x00\x00\x20`), regexp.MustCompile(`^\+\x00\x00\x00$`), regexp.MustCompile(`^\x00.\x00\x00\x02\x00\x00\x00.*\(IAGENT`), regexp.MustCompile(`^..\x00\x00\x04\x00\x00\x00"\x00..\(DESCRIPTION=`), regexp.MustCompile(`^\x00.\x00\x00[\x02\x04]\x00\x00\x00.*\(`), regexp.MustCompile(`^\x00.\x00\x00[\x02\x04]\x00\x00\x00.*TNSLSNR`), regexp.MustCompile(`^\x00,\x00\x00\x04\x00\x00"`), }, }, }, } // mongodb serviceRules["mongodb"] = serviceRule{ Tls: false, DataGroup: []ruleData{ { ActionSend, []byte("\x41\x00\x00\x00\x3a\x30\x00\x00\xff\xff\xff\xff\xd4\x07\x00\x00\x00\x00\x00\x00test.$cmd\x00\x00\x00\x00\x00\xff\xff\xff\xff\x1b\x00\x00\x00\x01serverStatus\x00\x00\x00\x00\x00\x00\x00\xf0\x3f\x00"), nil, }, { ActionRecv, nil, []*regexp.Regexp{ regexp.MustCompile(`(?s)^.*version([: "]+)([.\d]+)"`), regexp.MustCompile(`(?s)^\xcb\x00\x00\x00....:0\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa7\x00\x00\x00\x01uptime\x00\x00\x00\x00\x00\x00 ` + "`" + `@\x03globalLock\x009\x00\x00\x00\x01totalTime\x00\x00\x00\x00\x7c\xf0\x9a\x9eA\x01lockTime\x00\x00\x00\x00\x00\x00\xac\x9e@\x01ratio\x00!\xc6\$G\xeb\x08\xf0>\x00\x03mem\x00<\x00\x00\x00\x10resident\x00\x03\x00\x00\x00\x10virtual\x00\xa2\x00\x00\x00\x08supported\x00\x01\x12mapped\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01ok\x00\x00\x00\x00\x00\x00\x00\xf0\?\x00$`), regexp.MustCompile(`(?s)^.\x00\x00\x00....:0\x00\x00\x01\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\+\x00\x00\x00\x02errmsg\x00\x0e\x00\x00\x00need to login\x00\x01ok\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`), regexp.MustCompile(`(?s)^.\x00\x00\x00....:0\x00\x00\x01\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00.\x00\x00\x00\x01ok\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02errmsg\x00.\x00\x00\x00not authorized on`), }, }, }, } // memcached serviceRules["memcached"] = serviceRule{ Tls: false, DataGroup: []ruleData{ { ActionSend, []byte("stats\\n"), nil, }, { ActionRecv, nil, []*regexp.Regexp{ regexp.MustCompile(`(?s)^STAT pid \d`), regexp.MustCompile(`(?s)^ERROR\r\n`), }, }, }, } // onlyRecv 仅接收数据的服务列表 for k, m := range serviceRules { if len(m.DataGroup) == 1 { onlyRecv = append(onlyRecv, k) } } }
?ps:总体来说,这个代码段通过发送特定数据并检查返回数据中是否包含特定标识来识别不同服务的存在。如果匹配成功,则确定该服务在特定端口上运行。
此文件主要处理从 HTML 文档中提取标题以及获取页面重定向的目标 URL。
首先:定义变量,设置获取title、contenttype、refresh、replace的正则
var (
cutset = "\n\t\v\f\r"
reTitle = regexp.MustCompile(`(?im)<\s*title.*>(.*?)<\s*/\s*title>`)
reContentType = regexp.MustCompile(`(?im)\s*charset="(.*?)"|charset=(.*?)"\s*`)
reRefresh = regexp.MustCompile(`(?im)\s*content=['"]\d;url=['"](.*?)['"]`)
reReplace = regexp.MustCompile(`(?im)window\.location\.replace\(['"](.*?)['"]\)`)
)
这个函数用于从 HTML 页面中提取标题。它首先尝试使用 DOM 解析器解析文档,并提取其中的标题。如果解析出错,就使用正则表达式
reTitle
在 HTML 文档中查找标题标签,提取标题内容。最后对提取到的标题进行处理,去除空白字符和特殊符号func ExtractTitle(body []byte) (title string) { //先从dom中获取 titleDom, err := getTitleWithDom(body)//从dom节点中获取title //如果出错,则回退到用正则匹配 if err != nil { for _, match := range reTitle.FindAllString(string(body), -1) { title = match break } }else { title = renderNode(titleDom)//将html转换为字符串类型 } title = html.UnescapeString(trimTitleTags(title)) //删除多余字符 title = strings.TrimSpace(strings.Trim(title, cutset)) //去掉首尾的特殊字符 title = strings.ReplaceAll(title, "\n", "") title = strings.ReplaceAll(title, "\r", "") return title }
这个函数尝试使用 HTML 解析器来解析 HTML 文档,并找到
<title>
标签对应的节点。它通过递归方式遍历 HTML 文档的节点树,在遍历过程中寻找title
节点func getTitleWithDom(body []byte) (*html.Node, error){ var title *html.Node var crawler func(*html.Node) crawler = func(node *html.Node) {//用于遍历 HTML 文档的节点树,查找 <title> 节点。 if node.Type == html.ElementNode && node.Data == "title" { title = node return } for child := node.FirstChild; child != nil && title == nil; child = child.NextSibling { crawler(child) } } htmlDoc, err := html.Parse(bytes.NewReader(body))//使用 html.Parse 函数解析传入的 HTML 文档,将其转换为一个 HTML 文档对象(htmlDoc) if err != nil { return nil, err } crawler(htmlDoc) if title != nil { return title, nil } return nil, fmt.Errorf("title not found") }
这个函数将 HTML 节点转换为字符串形式,使用
html.Render
将节点内容写入缓冲区,然后将缓冲区的内容以字符串形式返回。func renderNode(n *html.Node) string{ var buf bytes.Buffer w := io.Writer(&buf) html.Render(w, n) //nolint return buf.String() }
这个函数用于处理提取到的标题字符串,去除
<title>
标签的开始和结束标记,只返回标题文本内容func trimTitleTags(title string) string { titleBegin := strings.Index(title, ">") titleEnd := strings.Index(title,"</") if titleEnd < 0 || titleBegin < 0 { return title } return title[titleBegin + 1 : titleEnd] }
这个函数用于从 HTML 文档中获取重定向的目标 URL。它首先使用正则表达式
reRefresh
在文档中查找重定向的目标 URL,如果未找到,则使用正则表达式reReplace
进行查找。如果找到,就返回第一个匹配到的重定向 URL。func GetLocation(body []byte) (location string) { for _, match := range reRefresh.FindAllStringSubmatch(string(body),1){ location = match[1] break } if location == "" { for _, match := range reReplace.FindAllStringSubmatch(string(body),1){ location = match[1] break } } return }
?ps:这些函数主要用于解析 HTML 文档,从中提取标题并检索重定向目标 URL。它们结合使用 DOM 解析器和正则表达式来从文档中提取所需的信息。
这个文件主要设置HTTP客户端和定义功能来处理响应体,识别其编码并限制最大读取大小。包含函数如下:
首先定义:
var ErrOverflow = errors.New("OverflowMax")
type Options struct {
}
- 创建并配置了一个具有特定传输配置的HTTP客户端,包括TLS设置、超时、最大连接数和禁用长连接等。
- 设置了一个自定义的
CheckRedirect
函数来限制最大重定向次数为两次。func newHttpClient(dialTimeout time.Duration) *http.Client { transport := &http.Transport{ TLSClientConfig: &tls.Config{//设置httpclient配置 InsecureSkipVerify: true, MinVersion: tls.VersionTLS10, }, DialContext: (&net.Dialer{ Timeout: dialTimeout, }).DialContext, MaxIdleConnsPerHost: 1, IdleConnTimeout: 100 * time.Millisecond, TLSHandshakeTimeout: 3 * time.Second, ExpectContinueTimeout: 3 * time.Second, DisableKeepAlives: true, ForceAttemptHttp2: false, Proxy: http.ProxyFromEnvironment, } // proxy //if options.ProxyUrl != "" { // proxyUrl, err := url.Parse(options.ProxyUrl) // if err != nil { // log.Fatalln(err) // } // transport.Proxy = http.ProxyURL(proxyUrl) //} return &http.Client{ Timeout : 3 * time.Second, Transport: transport, CheckRedirect: func(req *http.Request, via []*http.Request) error{ if len(via) >= 2{ return errors.New("stopped after 2 redirects") } return nil }, } }
- 根据响应头中的
Content-Encoding
字段确定响应体的编码格式。- 通过使用适当的读取器(例如
gzip.NewReader
和flate.NewReader
)处理不同的压缩格式,如 gzip 和 deflate。- 通过调用
readMaxSize
函数,限制响应体的最大大小为300KB。// getBody 识别响应Body的编码,读取body数据 func getBody(resp *http.Response) (body []byte, err error) { if resp.Body == nil || resp.Body == http.NoBody { return } var reader io.Reader switch resp.Header.Get("Content-Encoding") { case "gzip": reader, err = gzip.NewReader(resp.Body) case "deflate": reader = flate.NewReader(resp.Body) //case "br": // reader = brotli.NewReader(resp.Body) default: reader = resp.Body } if err == nil { body, err = readMaxSize(reader, 300*1024) // Max Size 300kb } return }
- 从
io.Reader
(r
)读取数据,同时限制最大尺寸(maxsize
)。- 为一个字节切片(
b
)分配内存,并以块的方式读取数据,将其附加到切片中,直到达到最大尺寸。- 如果达到了尺寸限制,函数返回到目前为止读取的数据以及一个
ErrOverflow
错误。//这个函数的作用是安全地从 io.Reader 中读取数据,并在达到指定的最大尺寸时停止读取,以避免读取过多的数据造成内存溢出或其他问题。 func readMaxSize(r io.Reader, maxsize int) ([]byte, error) { b := make([]byte, 0, 512) for { if len(b) >= maxsize { return b, ErrOverflow } if len(b) == cap(b) { // Add more capacity (let append pick how much). b = append(b, 0)[:len(b)] } n, err := r.Read(b[len(b):cap(b)]) b = b[:len(b)+n] if err != nil { if err == io.EOF { err = nil } return b, err } } }
ps:这些函数为主要目的是创建一个具有特定配置的HTTP客户端,并提供功能来处理响应体的解码,考虑到不同的压缩格式,并限制读取数据的最大尺寸,以防止潜在的过大响应引发问题。
这个文件主要实现了HTTP服务信息的探测和提取。主要函数如下:
先定义变量httpsTopPort,高频的https端口
var httpsTopPort = []uint16{443, 4443, 1443, 8443}
var httpClient *http.Client
- 接收一个 IP、端口和请求超时时间,用于在给定的 IP 地址和端口上探测 HTTP 服务信息。
- 根据指定的端口列表确定探测的协议(HTTP 或 HTTPS)。
- 使用
httpClient
对象发送 HTTP 请求,并根据响应提取信息。- 通过循环尝试 HTTP 和 HTTPS 请求,根据响应来获取信息,包括状态码、重定向 URL、服务器信息、标题、TLS 证书相关信息等。
- 通过
webfinger
包来分析响应的指纹信息,还会尝试提取页面的 favicon。func ProbeHttpInfo(ip net.IP, _port uint16, dialTimeout time.Duration) (httpInfo *port.HttpInfo, isDailErr bool) { if httpClient == nil { httpClient = newHttpClient(dialTimeout) } var err error var rewriteUrl string var body []byte var resp *http.Response var schemes []string if util.IsUint16InList(_port, httpsTopPort) { //检测请求的_port是否在httpsTopPort列表中 schemes = []string{"https", "http"} }else { schemes = []string["http", "https"} } for _, scheme := range schemes { //循环使用https和http去请求 var rewriteNum int url2 := fmt.Sprintf("%s://%s:%d/", scheme, ip.String(), _port) goReq: resp, body, err = getReq(url2) //获取响应包和响应body if err != nil { if strings.HasSuffix(err.Error(), ioTimeoutStr) || strings.Contains(err.Error(), regusedStr) { return nil, true } continue } if resp != nil { if resp.ContentLength == -1 { resp.ContentLength = int64(len(body)) } //先在响应头中获取重定向url rewriteUrl2, _ := resp.Location() //重新获取url,提取响应包头中的location参数值 if rewriteUrl2 != nil { rewriteUrl = rewriteUrl2.String() }else { rewriteUrl = "" } //其次在body中获取重定向url location := GetLocation(body) //在title.go中,用于获取body中的location 的url,即重定向url if rewriteUrl == "" && location != "" { rewriteUrl = location } if location != "" && rewriteNum < 3 { if !strings.HasPrefix(location, "http") { //判断location前缀中是否http开头,如果不是,则进入下一个if if strings.HasPrefix(location, "/") { resp.Request.URL.Path = location }else { resp.Request.URL.Path = resp.Request.URL.Path[:strings.LastIndex(resp.Request.URL.Path, "/")+1] + location//将 HTTP 请求的 URL 路径修改为原路径中最后一个斜杠之前的部分,再添加上 location 变量的值。这样,新的路径将以最后一个斜杠结尾,然后连接上 location 的值。 } location = resp.Request.URL.String() } url2 = location rewriteNum++ goto goReq } httpInfo = new(port.HttpInfo) //在port.go文件中,为HttpInfo结构体 httpInfo.Url = resp.Request.URL.String() httpInfo.StatusCode = resp.StatusCode httpInfo.ContentLen = int(resp.ContentLength) httpInfo.Location = rewriteUrl httpInfo.Server = resp.Header.Get("Server") httpInfo.Title = ExtracTitle(body) //title.go文件中,获取响应包中的title if resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 { //如果是https,获取tls握手证书的信息 httpInfo.TlsCN = resp.TLS.PeerCertificates[0].Subject.CommonName //证书主题部分的通用名称信息 httpInfo.TlsDNS = resp.TLS.PeerCertificates[0].DNSNames //证书中包含的DNS名称列表 } //指纹信息 err = webfinger.ParseWebFingerData(webfinger.DefFingerData) if err == nil { resp.Body = io.NopCloser(bytes.NewReader(body)) httpInfo.Fingers = webfinger.WebFingerIdent(resp) //j检测识别响应头和响应body识别指纹 //favicon fau := webfinger.FindFaviconUrl(string(body)) //获取响应body中的favionUrl if fau != ""{ if !strings.HasPrefix(fau, "http") { fau = resp.Request.URL.String() + fau } _, body2, err2 := getReq(fau) //获取请求favicon的响应包信息 if err2 == nil && len(body2) != 0{ httpInfo.Fingers = append(httpInfo.Fingers, webfinger.WebFingerIdentByFavicon(body2)...)//检测favicon指纹 } } } if resp.StatusCode != 400 { break } } } return httpInfo, false }
- 构造一个 HTTP GET 请求,设置请求头,然后使用
httpClient
执行请求。- 从响应中读取内容,如果是文本类型则进行编码解析(尝试将非 UTF-8 编码的文本解析为 UTF-8)。
func getReq (url2 string) (resp *http.Response, body []byte, err error) { req, err := http.NewRequest(http.MethodGet, url2, http.Nobody) //创建get请求 if err != nil { return } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36") req.Header.Set("Accept-Encoding", "gzip, deflate") req.Close = true //关闭keepalive resp, err = httpClient.Do(req) if err != nil { return } if resp.Body != http.NoBody && resp.Body != nil { //如果body不为空且不为http.NoBody,则提取body body, _ = getBody(resp) //在http.go文件中,主要为识别body的编码,读取body数据 if contentTypes, _ := resp.Header["Content-Type"]; len(contentTypes) > 0 { if strings.Contains(contentTypes[0], "text") {//如果响应类型是文本类型,则对响应体进行解码,将非utf-8编码的文本解析为utf-8 _body, err2 := DecodeData(body, resp.Header) if err2 == nil { body = _body } resp.Body = io.NopCloser(bytes.NewReader(body)) } } } return }
ps:这些函数主要是尝试使用 HTTP 和 HTTPS 进行请求,根据响应提取 HTTP 服务的相关信息,包括标题、服务器信息、TLS 证书信息以及其他可能的指纹信息,为后续服务识别和指纹探测提供数据支持。
这部分的功能主要围绕在对http和https指纹的识别,通过网页titile和icon的hash进行指纹判断;其次是针对端口进行服务识别,主要匹配规则库在“fingerprint-webfinger-finger.json” 文件和“core-port-fingerprint-rules.go”文件中。