跳到主要内容

工程实践:子域名收集联动 Yak 漏扫插件(安全研发启蒙)

· 阅读需 9 分钟
Yak Project
网络安全垂直语言团队

上期讲了如何低成本写一个被动扫描脚本 ,这期写一个主动扫描脚本。Yakit的基础安全工具中有一个子域名收集插件十分方便,本篇文章对这个插件做点改造,让它与漏扫插件联动,输入目标就可以自动化扫描漏洞。

Yak基础知识

在开始之前了解一下需要用到的库函数

如图,子域名扫描需要用到subdomain库,其中核心就是subdomain.Scan函数,其余都是参数函数。subdomain.Scan的默认参数如下

参数名默认值
dnsServer114.114.114.114/8.8.8.8
eachQueryTimeout3s
eachSearchTimeout10s
mainDict3164条数据的默认字典
maxDepth5
recursivetrue
recursiveDict163条数据的默认字典(子域名的字典)
targetConsurrent10
targetTimeout300s
wildcardStopfalse
workerConcurrent50

大多数时候使用默认参数就足够了,所以做一次子域名扫描如下

res, err := subdomain.Scan("baidu.com")
if err != nil {
yakit.Error("构建子域名扫描失败:%v", err)
die(err)
}

subdomain.Scan的返回值类型是 chan *subdomain.SubdomainResult

*subdomain.SubdomainResult的结构体声明等信息在官网文章里有详细介绍-Yak的常见库使用教程-[subdomain] 子域名收集。

编写插件

现在就将子域名扫描与漏扫插件联动起来,先对目标做子域名扫描,等待扫描结束后,再找出重点的ip的c段,对其进行端口扫描和漏洞扫描,流程很简单,流程图如下。

PART.1

子域名扫描

target = "baidu.com"
res, err := subdomain.Scan(target) // res的类型是chan *subdomain.SubdomainResult
if err != nil {
yakit.Error("构建子域名扫描失败:%v", err)
die(err)
}

res是一个channel,所以subdomain.Scan执行时不会阻塞,每生成一条执行结果都会写入到res中,遍历res就可以拿到执行结果。

PART.2

端口扫描

本篇基于子域名收集插件做修改,插件中记录了所有被解析的ip和c段的出现次数,存储到变量ipCounter和cClassCollector中,所以我打算从中找出重点ip和重点c段做端口扫描,再做漏洞扫描。

// 通过判断ip和c段出现次数判断是否需要扫描
ipThreshold = 5
cClassCollectorThreshold = 10
ports = "21,22,443,445,80,8000-8004,3306,3389,5432,6379,8080-8084,7000-7005,9000-9002,8443,7443,9443,7080,8070"

ipForScan = []
// 添加所有超过阈值的c段
yakit.Info("统计需要扫描的ip/c段")
for c,count = range cClassCollector{
if count > cClassCollectorThreshold{
ipForScan = append(ipForScan,c)
}
}
for ip,count = range ipCounter{
if count > ipThreshold{
network = str.IPv4ToCClassNetwork(ip)
network = network[0]
// 如果ip数量超过了阈值,且不在已添加的c段中,则将ip添加进扫描
if cClassCollector[network] < cClassCollectorThreshold{
ipForScan = append(ipForScan,ip)
}
}
}

// 开始扫描
targets = str.Join(ipForScan,",")
res, err = servicescan.Scan(targets,ports,servicescan.probeTimeout(10))
if err != nil {
yakit.Error("servicescan %v failed: %s", t)
return
}

PART.3

调用port-scan插件

调用方式和上一篇的大致相同,不过这次端口扫描插件是通过HandleServiceScanResult方法调用

// 初始化manager
yakit.Info("开始漏洞扫描")
manager, err = hook.NewMixPluginCaller()
// 重定向输出,和上一篇不同,这里使用yakit.Info向UI输出
manager.SetFeedback(func(i){
msg = json.loads(i.Message)
data = msg.content.data
level = msg.content.level
switch msg.content.level{
case "info":
yakit.Info(data)
case "error":
yakit.Error(data)
default:
yakit.Info("收到信息,不支持的信息类型: [%s] %s",level,data)
}
})
if err != nil {
println("build mix plugin caller failed: %s", err)
die(err)
}
yakScriptsChan = db.YieldYakScriptAll()
for yakscript = range yakScriptsChan{
if yakscript.Type == "port-scan"{
manager.LoadPlugin(yakscript.ScriptName)
}
}
// 调用插件
for result = range res {
yakit.Info("正在扫描目标: %s:%d", result.Target,result.Port)
manager.HandleServiceScanResult(result)
}
manager.Wait()

PART.4

优化信息展示

至此子域名扫描和漏洞扫描联动就完成了,但是还需要一些优化。如图,可以看见,子域名收集插件会将信息以表格的形式展现出来,所以需要一些优化,将端口扫描与漏洞扫描的结果也通过ui展示出来。

这里主要需要用到 yakit.EnableTableyakit.TableData函数,yakit.EnableTable是启动一个页表格,需要两个参数,例如 yakit.EnableTable("用户", ["姓名", "年龄"]),第一个参数是表名,相当于excel的工作簿名,第二个参数是字段名。yakit.TableData 用于向表格输出数据,需要两个参数,例如yakit.TableData("用户", {"姓名":"张三","年龄":20}),第一个参数指定输出到的表,第二个参数是数据。

yakit.EnableTable("端口扫描信息表", ["主机地址", "HTML Title","指纹"]) // 创建一页表格
// outputTablePortScan方法用于向表格写入数据
outputTablePortScan = func(addr, title,fingerPrints) {
data = make(map[string]var)
data["主机地址"] = addr
data["HTML Title"] = title
data["指纹"] = fingerPrints
yakit.TableData("端口扫描信息表", data)
}

端口扫描结果这里稍作修改就可以了

// 调用插件
for result = range res {
outputTablePortScan(
sprintf("%s:%d", result.Target,result.Port),
result.GetHtmlTitle(),
result.GetServiceName(),
)
yakit.Info("正在扫描目标: %s:%d", result.Target,result.Port)
manager.HandleServiceScanResult(result)
}
manager.Wait()

PART.5

UI优化

可能很多人没注意,Yakit插件还支持UI联动,如图

联动后的效果就和"端口扫描/指纹"插件效果类似,所以代码还可以再优化一下。启动联动端口扫描插件后,将加载插件部分代码改成如下

scriptNameFile = cli.String("--yakit-plugin-file")
scriptNames = x.If(scriptNameFile != "", x.Filter(
x.Map(
str.Split(string(file.ReadFile(scriptNameFile)[0]), "|"),
func(e){return str.TrimSpace(e)},
), func(e){return e!=""}), make([]string))

x.Foreach(scriptNames, func(e){
manager.LoadPlugin(e)
})

再将插件添加到菜单栏

PART.6

测试脚本

运行脚本测试如图

PART.7

最终脚本

yakit.AutoInitYakit()
// 通过判断ip和c段出现次数判断是否需要扫描
ipThreshold = cli.Int("ipThreshold")
cClassCollectorThreshold = cli.Int("cClassCollector")
target = cli.String("target", cli.setDefault("uestc.edu.cn"))
ports = cli.String("ports")

notRecursive = cli.Bool("not-recursive", cli.setHelp("设置是否递归爆破?"))
wildcardToStop = cli.Bool("wildcard-to-stop") // 泛解析停止

res, err := subdomain.Scan(target, subdomain.recursive(!notRecursive), subdomain.wildcardToStop(wildcardToStop))
if err != nil {
yakit.Error("构建子域名扫描失败:%v", err)
die(err)
}

/**
type palm/common/subdomain.(SubdomainResult) struct {
FromTarget: string
FromDNSServer: string
FromModeRaw: int
IP: string
Domain: string
Tags: []string
StructMethods(结构方法/函数):
PtrStructMethods(指针结构方法/函数):
func Hash() return(string)
func Show()
func ToString() return(string)
}
*/

yakit.Info("开始准备处理子域名收集的结果")
yakit.EnableTable("端口扫描信息表", ["主机地址", "HTML Title","指纹"])
yakit.EnableTable("涉及C段表", ["涉及C段"])
yakit.EnableTable("子域名表", ["Domain", "IP"])
outputTableCClass = func(network) {
data = make(map[string]var)
data["涉及C段"] = network
yakit.TableData("涉及C段表", data)
}
outputTable = func(domain, ip) {
data = make(map[string]var)
data["Domain"] = domain
data["IP"] = ip
yakit.TableData("子域名表", data)
}

outputTablePortScan = func(addr, title,fingerPrints) {
data = make(map[string]var)
data["主机地址"] = addr
data["HTML Title"] = title
data["指纹"] = fingerPrints
yakit.TableData("端口扫描信息表", data)
}

count = 0
savedCount = 0
mux = sync.NewLock()
statusAddCount = func() {
mux.Lock()
defer mux.Unlock()

count++
yakit.StatusCard("已保存/已收集", str.f("%v/%v", savedCount, count))
}
statusSavedAddCount = func() {
mux.Lock()
defer mux.Unlock()

savedCount++
yakit.StatusCard("已保存/已收集", str.f("%v/%v", savedCount, count))
}

swg = sync.NewSizedWaitGroup(40)
submitToDB = func(domain, ip) {
swg.Add()
go func{
defer swg.Done()
yakit.SaveDomain(domain, ip)
statusSavedAddCount()
}
}

ipCounter = make(map[string]int)
cClassCollector = make(map[string]int)
for result = range res {
ip = result.IP
if ipCounter[ip] == undefined {
ipCounter[ip] = 0
}
ipCounter[ip] ++
if ipCounter[ip] > 10 {
yakit.StatusCard(sprintf("IP:%v 解析数", ip), ipCounter[result.IP])
}

result.Show()
statusAddCount()
outputTable(result.Domain, result.IP)
network = str.IPv4ToCClassNetwork(result.IP)
network = network[0]
if cClassCollector[network] == undefined {
cClassCollector[network] = 0
outputTableCClass(network)
}
cClassCollector[network] ++
// yakit.StatusCard(sprintf("C段:%v 主机数", network), cClassCollector[network])

submitToDB(result.Domain, result.IP)
}

swg.Wait()

ipForScan = []
// 添加所有超过阈值的c段
yakit.Info("统计需要扫描的ip/c段")
for c,count = range cClassCollector{
if count > cClassCollectorThreshold{
ipForScan = append(ipForScan,c)
}
}
for ip,count = range ipCounter{
if count > ipThreshold{
network = str.IPv4ToCClassNetwork(ip)
network = network[0]
// 如果ip数量超过了阈值,且不在已添加的c段中,则将ip添加进扫描
if cClassCollector[network] < cClassCollectorThreshold{
ipForScan = append(ipForScan,ip)
}
}
}

// 开始扫描
targets = str.Join(ipForScan,",")
yakit.Info("开始端口扫描,target: %s,port: %s",targets,ports)
res, err = servicescan.Scan(targets,ports,servicescan.probeTimeout(10))
if err != nil {
yakit.Error("servicescan %v failed: %s", t)
return
}
// 初始化manager
yakit.Info("开始漏洞扫描")
manager, err = hook.NewMixPluginCaller()
if err != nil {
println("build mix plugin caller failed: %s", err)
die(err)
}
manager.SetFeedback(func(i){
msg = json.loads(i.Message)
data = msg.content.data
level = msg.content.level
switch msg.content.level{
case "info":
yakit.Info(data)
case "error":
yakit.Error(data)
default:
yakit.Info("收到信息,不支持的信息类型: [%s] %s",level,data)
}
})
scriptNameFile = cli.String("--yakit-plugin-file")
scriptNames = x.If(scriptNameFile != "", x.Filter(
x.Map(
str.Split(string(file.ReadFile(scriptNameFile)[0]), "|"),
func(e){return str.TrimSpace(e)},
), func(e){return e!=""}), make([]string))

x.Foreach(scriptNames, func(e){
manager.LoadPlugin(e)
})
// 调用插件
for result = range res {
outputTablePortScan(
sprintf("%s:%d", result.Target,result.Port),
result.GetHtmlTitle(),
result.GetServiceName(),
)
yakit.Info("正在扫描目标: %s:%d", result.Target,result.Port)
manager.HandleServiceScanResult(result)
}
manager.Wait()

总结

本篇文章通过对子域名扫描与端口扫描插件联动实现自动化对某域名目标的漏洞扫描,Yakit 的库函数不仅可以方便地调用插件、实现各种联动,还可以自动生成 UI,表格数据也支持一键导出,使用起来较为便捷。

更新通知

往期推荐

安全研发启蒙课:低成本实现的被动扫描工具

Yakit功能介绍之【插件商店】最全指南

基础设施:Yaklang Java字节码能力支持

Yaklang XSS 检测启发式算法(被动扫描插件)

Low Code, Full Turing: Yaklang 分布式引擎 SaaS 化


本文首发于 Yak Project 公众号,阅读原文