近期ScopeSentryt更新了插件系统,可以用这个插件系统将一些指纹识别工具进行集成,实现"多引擎"的指纹识别功能,本文以EHole为例编写一个指纹识别插件。
除了实现指纹识别的功能,也可以集成其他的工具,并且所有收集到的数据都可以进行二次处理再存储。比如丰富资产标签,拿到资产信息之后通过编写插件去获取域名的关联信息然后放入tag中存入结果,再比如获取到url、爬虫信息时,可以对一些特定的参数进行一些特定的测试。
模块流程图:
高清图:https://raw.githubusercontent.com/Autumn-27/ScopeSentry/main/%E6%B5%81%E7%A8%8B%E5%9B%BE.svg
插件下载:
https://plugin.scope-sentry.top/plugin/4fb56e0137e9fda3e9c76e9446868fbb
效果:
插件模板
git clone https://github.com/Autumn-27/ScopeSentry-Plugin-Template.git
插件模板中提供了AssetHandle模块和SubdomainScan模块的demo
https://github.com/Autumn-27/ScopeSentry-Plugin-Template/tree/main/plugin/AssetHandle
https://github.com/Autumn-27/ScopeSentry-Plugin-Template/tree/main/plugin/SubdomainScan
系统模块
插件系统将整个流程分为了13个模块,所以创建插件也是在这些模块下创建的。
"TargetHandler",
"SubdomainScan",
"SubdomainSecurity",
"AssetMapping",
"PortScanPreparation",
"PortScan",
"PortFingerprint",
"AssetHandle",
"URLScan",
"URLSecurity",
"WebCrawler",
"DirScan",
"VulnerabilityScan",
创建插件
首先下载插件模板
https://github.com/Autumn-27/ScopeSentry-Plugin-Template.git
在plugin目录下对应的模块,创建插件文件夹(一般为插件名)并在该文件夹下创建两个文件plugin.go、info.json
info.json
其中info.json包含插件的基本信息。parameter是插件的默认参数,help是插件的提示字符 。name需要和plugin.go中的GetName保持一致,module也需要准确。
{
"help":"-finger 指纹路径 -thread 并发限制",
"parameter":"-finger {dict.finger.finger.json} -thread 20",
"name":"EHole",
"module": "AssetHandle",
"version":"v1.0",
"introduction":"EHole 指纹识别/EHole Fingerprint recognition"
}
- help:参数提示信息
- parameter:参数
- name:插件名称
- module:模块(模块需要准确)
- version:版本
- introduction:插件的简介
plugin.go
package plugin
func GetName() string {
return ""
}
func Install() error {
return nil
}
func Check() error {
return nil
}
func Uninstall() error {
return nil
}
func Execute(input interface{}, op options.PluginOption) (interface{}, error) {
return nil, nil
}
plugin.go文件中包名需要为plugin
plugin.go需要五个方法
GetName
返回插件名称
func GetName() string {
return "EHole"
}
Install
在插件加载的时候会先执行install方法,如果插件需要下载一些可执行文件那么可以在Install方法中写。
func Install() error {
return nil
}
Check
执行完Install方法后会执行check方法,如果有些插件需要一些验证可以在该方法内完成
func Check() error {
return nil
}
Uninstall
当删除插件的时候会执行Uninstall方法
func Uninstall() error {
return nil
}
Execute
插件实际上运行的就是Execute方法,该方法接收两个参数input interface{}和 op options.PluginOption
PluginOption的定义在
https://github.com/Autumn-27/ScopeSentry-Plugin-Template/blob/main/internal/options/plugin.go
type PluginOption struct {
Name string
Module string
Parameter string
PluginId string
ResultFunc func(interface{})
Custom interface{}
TaskId string
TaskName string
Log func(msg string, tp ...string)
Ctx context.Context
}
Name 为插件名称
Module 为插件所属模块
Parameter 为插件参数
在创建插件时,填写参数信息可以使用{}来引用字典和端口
{dict.subdomain.default} 这个参数会替换为字典中subdomain类别的default名称的文件id
和
{port.top1000} 这个参数会替换为端口管理处的name为top1000的端口
参数解析参考(调用工具类的ParseArgs方法):
args, err := utils.Tools.ParseArgs(parameter, "cdncheck", "screenshot")
if err != nil {
} else {
for key, value := range args {
if value != "" {
switch key {
case "cdncheck":
cdncheck = value
case "screenshot":
if value == "true" {
screenshot = true
}
default:
continue
}
}
}
}
PluginId 插件id
ResultFunc 插件结果回调函数
比较特殊的是AssetHandle、PortScanPreparation、PortFingerprint模块,input输入为引用,直接修改input就是对结果的修改。
其余模块在获取到结果之后调用op.ResultFunc回调函数来返回结果
Custom 自定义的参数,未来用来拓展
TaskId 任务id
可以利用任务id来进行全局的去重
TaskName 任务名称
Log 日志打印函数
op.Log("test") // info类型日志
op.Log("test", "e") // error类型日志
op.Log("test", "d") // debug类型日志
op.Log("test", "w") // warning类型日志
插件可以调用的内置方法
目前由于插件系统的限制,无法调用第三方的库,所以在系统中内置了一些方法,可供调用,具体如下:
github.com/Autumn-27/ScopeSentry-Scan/pkg/logger // 日志包
github.com/Autumn-27/ScopeSentry-Scan/pkg/utils // 工具包,提供了一系列dns、request、tools方法
github.com/Autumn-27/ScopeSentry-Scan/internal/options
github.com/Autumn-27/ScopeSentry-Scan/internal/bigcache // 缓存包,可以列用该模块进行去重
github.com/Autumn-27/ScopeSentry-Scan/internal/config // 配置
github.com/Autumn-27/ScopeSentry-Scan/internal/contextmanager // 任务上下文
github.com/Autumn-27/ScopeSentry-Scan/internal/global // 全局变量
github.com/Autumn-27/ScopeSentry-Scan/internal/interfaces
github.com/Autumn-27/ScopeSentry-Scan/internal/mongodb // mongodb连接,可以直接调用进行存储数据
github.com/Autumn-27/ScopeSentry-Scan/internal/notification // 通知模块,可以进行发送通知信息
github.com/Autumn-27/ScopeSentry-Scan/internal/pool // 协程池管理,可以自定义协程模块进行全局的并发限制
github.com/Autumn-27/ScopeSentry-Scan/internal/redis // redis连接
github.com/Autumn-27/ScopeSentry-Scan/internal/results // 结果发送模块
github.com/Autumn-27/ScopeSentry-Scan/internal/types
上边提到的包下边所有方法都可以调用
各模块的输入输出
参考官网的https://scope-sentry.top/guide/2ixl876k/#targethandler
需要说明的,各模块的输入和输出无需完全遵循官网的标准,例如在目标处理模块,在获取到需要处理的目标时,可以直接声明一个types.DomainSkip类型的数据,将端口设置为80,然后返回到结果处,在目标处理模块接收结果的地方判断如果数据类型不是本模块的类型,则会发送到下个模块,那么这个DomainSkip类型的数据就会一路发送到PortScan模块进行端口扫描。其余模块也类似,如果判断不是该模块的类型会直接往后发送。
其中端口扫描的结果最后都会转换成types.AssetOther 或 types.AssetHttp。这两种数据会一直发送到最后一个漏洞扫描模块,url扫描的结果以及爬虫的结果也会发送到漏洞扫描模块,只是目前漏洞扫描模块只利用了AssetOther和AssetHttp。
EHole插件编写
其实就是将EHole官方https://github.com/EdgeSecurityTeam/EHole解析规则的代码抠出来放到Execute方法中
package plugin
import (
"encoding/json"
"fmt"
"github.com/Autumn-27/ScopeSentry-Scan/internal/global"
"github.com/Autumn-27/ScopeSentry-Scan/internal/options"
"github.com/Autumn-27/ScopeSentry-Scan/internal/types"
"github.com/Autumn-27/ScopeSentry-Scan/pkg/utils"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
)
func GetName() string {
return "EHole"
}
func Install() error {
return nil
}
func Check() error {
return nil
}
func Uninstall() error {
return nil
}
type Fingerprints struct {
Fingerprint []Fingerprint
}
type Fingerprint struct {
Cms string
Method string
Location string
Keyword []string
}
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
}
func isregular(str string, keyword []string) bool {
var x bool
x = true
for _, k := range keyword {
re := regexp.MustCompile(k)
if re.Match([]byte(str)) {
x = x && true
} else {
x = x && false
}
}
return x
}
func RemoveDuplicatesAndEmpty(a []string) (ret []string) {
a_len := len(a)
for i := 0; i < a_len; i++ {
if (i > 0 && a[i-1] == a[i]) || len(a[i]) == 0 {
continue
}
ret = append(ret, a[i])
}
return
}
func Execute(input interface{}, op options.PluginOption) (interface{}, error) {
// 加载EHole指纹
data, ok := input.(*types.AssetHttp)
if !ok {
// 说明不是http的资产,直接返回
return nil, nil
}
parameter := op.Parameter
var filgerFile string
thread := 20
if parameter != "" {
args, err := utils.Tools.ParseArgs(parameter, "finger", "thread")
if err != nil {
} else {
for key, value := range args {
if value != "" {
switch key {
case "finger":
filgerFile = value
case "thread":
thread, _ = strconv.Atoi(value)
}
}
}
}
}
if filgerFile == "" {
op.Log("EHole 指纹文件为空", "w")
return nil, nil
}
fingerFilePath := filepath.Join(global.DictPath, filgerFile)
content, err := os.ReadFile(fingerFilePath)
if err != nil {
op.Log(fmt.Sprintf("read finger error: %v", err))
return nil, nil
}
var fingers Fingerprints
err = json.Unmarshal(content, &fingers)
if err != nil {
op.Log(fmt.Sprintf("json to fingers error: %v", err))
return nil, nil
}
semaphore := make(chan struct{}, thread)
var wg sync.WaitGroup
// 使用 sync.Map 来保证并发安全
uniqueCms := sync.Map{}
for _, finp := range fingers.Fingerprint {
select {
case <-op.Ctx.Done():
break
default:
semaphore <- struct{}{} // 占用一个槽,限制并发数量
wg.Add(1)
go func(finger Fingerprint) {
defer func() {
<-semaphore // 释放一个槽,允许新的goroutine开始
wg.Done()
}()
if finger.Location == "body" {
if finger.Method == "keyword" {
if iskeyword(data.ResponseBody, finger.Keyword) {
// 使用 sync.Map 进行并发安全操作
uniqueCms.Store(finger.Cms, true)
}
}
if finger.Method == "faviconhash" {
if data.FavIconMMH3 == finger.Keyword[0] {
uniqueCms.Store(finger.Cms, true)
}
}
if finger.Method == "regular" {
if isregular(data.ResponseBody, finger.Keyword) {
uniqueCms.Store(finger.Cms, true)
}
}
}
if finger.Location == "header" {
if finger.Method == "keyword" {
if iskeyword(data.RawHeaders, finger.Keyword) {
uniqueCms.Store(finger.Cms, true)
}
}
if finger.Method == "regular" {
if isregular(data.RawHeaders, finger.Keyword) {
uniqueCms.Store(finger.Cms, true)
}
}
}
if finger.Location == "title" {
if finger.Method == "keyword" {
if iskeyword(data.Title, finger.Keyword) {
uniqueCms.Store(finger.Cms, true)
}
}
if finger.Method == "regular" {
if isregular(data.Title, finger.Keyword) {
uniqueCms.Store(finger.Cms, true)
}
}
}
}(finp)
}
}
wg.Wait() // 等待所有goroutine完成
// 从 sync.Map 中获取去重后的结果
var result []string
existingMap := make(map[string]bool) // 用于忽略大小写去重
// 合并 data.Technologies 中的元素
for _, v := range data.Technologies {
lowerV := strings.ToLower(v) // 转换为小写进行去重
if _, exists := existingMap[lowerV]; !exists {
result = append(result, v) // 保持原始大小写
existingMap[lowerV] = true // 标记该元素已添加
}
}
// 将 uniqueCms 中的结果加入到 Technologies 中
uniqueCms.Range(func(key, value interface{}) bool {
cms := key.(string)
lowerCms := strings.ToLower(cms) // 转换为小写进行去重
if _, exists := existingMap[lowerCms]; !exists {
result = append(result, cms) // 保持原始大小写
existingMap[lowerCms] = true // 标记该元素已添加
}
return true
})
// 将去重后的结果赋值给 Technologies
data.Technologies = result
return nil, nil
}
主要是增加了一个参数用来设置规则文件以及并发设置
parameter := op.Parameter
var filgerFile string
thread := 20
if parameter != "" {
args, err := utils.Tools.ParseArgs(parameter, "finger", "thread")
if err != nil {
} else {
for key, value := range args {
if value != "" {
switch key {
case "finger":
filgerFile = value
case "thread":
thread, _ = strconv.Atoi(value)
}
}
}
}
}
插件调试
模拟实际环境运行插件
插件是实现是利用yaegi实现的
在plugin_cmd.go中编写测试用例
func main() {
global.DatabaseEnabled = false
Init()
global.AppConfig.Debug = false
_, filePath, _, ok := runtime.Caller(0)
if !ok {
log.Fatalf("无法获取当前文件路径")
}
parentDir := filepath.Dir(filePath)
plgPath := filepath.Join(parentDir, "..", "..", "plugin")
fmt.Println(plgPath)
}
这段代码不需要动,用来找到插件所在的相对路径。
这段代码是使用yaegi来加载些的差劲
func TestEHole(plgPath string) {
// plugin id
plgId := utils.Tools.GenerateRandomString(8)
// plugin module name
plgModule := "AssetHandle"
// plugin path
plgPath = filepath.Join(plgPath, "AssetHandle", "ehole", "plugin.go")
plugin, err := plugins.LoadCustomPlugin(plgPath, plgModule, plgId)
if err != nil {
return
}
fmt.Printf("plugin name: %v\n", plugin.GetName())
fmt.Printf("plugin module: %v\n", plugin.GetModule())
fmt.Printf("plugin id: %v\n", plugin.GetPluginId())
result := make(chan interface{})
// 设置插件参数
plugin.SetParameter("-finger dwa -thread 20")
// 设置任务id
plugin.SetTaskId("1111")
// 设置任务名称
plugin.SetTaskName("demo")
// 设置结果输出chan
plugin.SetResult(result)
fmt.Printf("AssetHttpData original Technologies: %v\n", AssetHttpData.Technologies)
// 调用执行插件,这里也就是执行插件的Execute,这里和插件不同只需要input,另一个参数实际上是程序自动帮助完成的
_, err = plugin.Execute(&AssetHttpData)
if err != nil {
return
}
fmt.Printf("AssetHttpData Technologies%v\n", AssetHttpData.Technologies)
}
可调试的测试用例
上边的测试样例是使用yaegi进行加载的,也就是按照实际插件运行的方式来调用插件,但是这样的话就无法对插件进行调试,所以还需要一个可以调试的测试样例,非常简单,直接调用即可
import (
plugin "github.com/Autumn-27/ScopeSentry-Scan/plugin/AssetHandle/ehole"
)
// 引入插件,重命名为plugin
// 手动设置op参数,然后直接调用插件的Execute方法
func TestEHoleDebug() {
op := options.PluginOption{
Name: "EHole",
Module: "AssetHandle",
Parameter: "-finger dwa -thread 20",
PluginId: "11111",
Ctx: contextmanager.GlobalContextManagers.GetContext("111111"),
}
plugin.Execute(&AssetHttpData, op)
}