毕设扫描器参数Fuzz第二篇:动态爬虫的创建启动和协程池

Posted 区块链市场观察家

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了毕设扫描器参数Fuzz第二篇:动态爬虫的创建启动和协程池相关的知识,希望对你有一定的参考价值。

文章目录

前言

在 《第一篇:数据的定义、读取和装配(爬虫数据和Payload数据)》文章中,我们完成了 URL 数据的读取、以及 Payload 的加载部分。

接下来要做的是发送请求对象,根据响应对象判断是否存在漏洞。

本篇文章要探究的,就是 CrawlerGo 动态爬虫发送请求和处理的过程,以求为参数 Fuzz 发送请求和处理响应提供参考和借鉴。

Crawlergo发送请求的代码节点

CrawlerGo 为动态爬虫装配数据的代码,前面我们基本阅读完毕。接下来看创建爬虫任务、以及启动爬虫部分的代码,查看具体的实现细节。

// crawlergo_cmd.go 代码 311 行

// 开始爬虫任务
	task, err := pkg.NewCrawlerTask(targets, taskConfig)
	if err != nil 
		logger.Logger.Error("create crawler task failed.")
		os.Exit(-1)
	
	……
	go handleExit(task)
	logger.Logger.Info("Start crawling.")
	task.Run()
	result := task.Result

// 追踪 task.Run()
// task_main.go 代码 173行
/*
开始当前任务
*/
func (t *CrawlerTask) Run() 
	defer t.Pool.Release()  // 释放协程池
	defer t.Browser.Close() // 关闭浏览器

	// 为 Targets 设置 reqsFromRobots 变量
	if t.Config.PathFromRobots 
		reqsFromRobots := GetPathsFromRobots(*t.Targets[0])
		logger.Logger.Info("get paths from robots.txt: ", len(reqsFromRobots))
		t.Targets = append(t.Targets, reqsFromRobots...)
	

	// 为 Targets 设置 reqsByFuzz 变量
	if t.Config.FuzzDictPath != "" 
		if t.Config.PathByFuzz 
			logger.Logger.Warn("`--fuzz-path` is ignored, using `--fuzz-path-dict` instead")
		
		reqsByFuzz := GetPathsByFuzzDict(*t.Targets[0], t.Config.FuzzDictPath)
		t.Targets = append(t.Targets, reqsByFuzz...)
	 else if t.Config.PathByFuzz 
		reqsByFuzz := GetPathsByFuzz(*t.Targets[0])
		logger.Logger.Info("get paths by fuzzing: ", len(reqsByFuzz))
		t.Targets = append(t.Targets, reqsByFuzz...)
	


	t.Result.AllReqList = t.Targets[:]

	// 创建 Request 对象用于 task 的初始化
	var initTasks []*model.Request

	// 遍历 Targets 数组
	for _, req := range t.Targets 
		if t.smartFilter.DoFilter(req) 
			logger.Logger.Debugf("filter req: " + req.URL.RequestURI())
			continue
		
		initTasks = append(initTasks, req)
		t.Result.ReqList = append(t.Result.ReqList, req)
	
	logger.Logger.Info("filter repeat, target count: ", len(initTasks))

	for _, req := range initTasks 
		if !engine2.IsIgnoredByKeywordMatch(*req, t.Config.IgnoreKeywords) 
			t.addTask2Pool(req)
		
	

	t.taskWG.Wait()

	// 对全部请求进行唯一去重
	todoFilterAll := make([]*model.Request, len(t.Result.AllReqList))
	for index := range t.Result.AllReqList 
		todoFilterAll[index] = t.Result.AllReqList[index]
	

	t.Result.AllReqList = []*model.Request
	var simpleFilter filter2.SimpleFilter
	for _, req := range todoFilterAll 
		if !simpleFilter.UniqueFilter(req) 
			t.Result.AllReqList = append(t.Result.AllReqList, req)
		
	

	// 全部域名
	t.Result.AllDomainList = AllDomainCollect(t.Result.AllReqList)
	// 子域名
	t.Result.SubDomainList = SubDomainCollect(t.Result.AllReqList, t.RootDomain)

设置代理调试寻找(未抓到包)

(1)设置代理参数,开启 Burpsuite,没抓到 HTTP 数据包。

--request-proxy http://127.0.0.1:8080 --output-json debug.json http://127.0.0.1/sqli-labs-master/Less-1/?id=1

(2)尝试内置 request-proxy 参数,添加 Value 字段设置参数默认值,从控制台截图可以看到设置成功了,但 Burp 仍然没有收到 HTTP 数据包。

&cli.StringFlag
				Name:        "request-proxy",
				Usage:       "all requests connect through defined proxy server.",
				Value:       "http://127.0.0.1:8080",
				Destination: &taskConfig.Proxy,
			,

查看 task_main.go 文件,搜索 Proxy,可以看到是在初始化浏览器时加载了代理。

	crawlerTask.Browser = engine2.InitBrowser(taskConf.ChromiumPath, taskConf.IncognitoContext, taskConf.ExtraHeaders, taskConf.Proxy, taskConf.NoHeadless)
	crawlerTask.RootDomain = targets[0].URL.RootDomain()

WireShark本地抓包(找到代码节点)

参考文章:wireShark抓取本地http包,分析状态

前往 Npcap官网 下载安装包,安装时记得勾选 Adapter for lookback traffic capture 选项。打开 WireShark 选择该过滤器监听。

调试 CrawlerGo,观察执行哪一部分代码后 HTTP 请求中出现了目标网站的访问记录。

经过简单对比,发现执行 t.taskWG.Wait() 代码后发送HTTP请求。

// 追踪 task.Run()
// task_main.go 代码 173行
/*
开始当前任务
*/
func (t *CrawlerTask) Run() 
	defer t.Pool.Release()  // 释放协程池
	defer t.Browser.Close() // 关闭浏览器

	// 为 Targets 设置 reqsFromRobots 变量

	// 为 Targets 设置 reqsByFuzz 变量

	t.Result.AllReqList = t.Targets[:]

	// 创建 Request 对象用于 task 的初始化

	// 遍历 Targets 数组
	
	// 遍历初始化任务
	for _, req := range initTasks 
		if !engine2.IsIgnoredByKeywordMatch(*req, t.Config.IgnoreKeywords) 
			t.addTask2Pool(req)
		
	

	t.taskWG.Wait()

	// 对全部请求进行唯一去重
	todoFilterAll := make([]*model.Request, len(t.Result.AllReqList))
	for index := range t.Result.AllReqList 
		todoFilterAll[index] = t.Result.AllReqList[index]
	

CrawlerGo设置多线程爬虫

爬虫调用代码

// crawlergo_cmd.go 代码 312 行
// 开始爬虫任务
	task, err := pkg.NewCrawlerTask(targets, taskConfig)
	if err != nil 
		logger.Logger.Error("create crawler task failed.")
		os.Exit(-1)
	
	……

	go handleExit(task)
	logger.Logger.Info("Start crawling.")
	task.Run()
	result := task.Result

创建爬虫任务

分析代码,该部分主要操作了如下三部分内容:

  • 给爬虫任务装配数据
  • 以无头模式启动浏览器
  • 创建协程池并设置最大协程数量
	// 开始爬虫任务
	task, err := pkg.NewCrawlerTask(targets, taskConfig)

// task_main.go 代码 70 行
/**新建爬虫任务
	接收配置好的 Request 、TaskConfig 对象
*/
func NewCrawlerTask(targets []*model.Request, taskConf TaskConfig) (*CrawlerTask, error) 
	// 定义爬虫任务的数据类型:字典
	crawlerTask := CrawlerTask
		Result: &Result,
		Config: &taskConf,
		smartFilter: filter2.SmartFilter
			SimpleFilter: filter2.SimpleFilter
				HostLimit: targets[0].URL.Host,
			,
		,
	

	// 单个目标,测试也使用单目标站点
	// 定义 newReq 接收 targets[0],确定通信协议
	if len(targets) == 1 
		_newReq := *targets[0]
		newReq := &_newReq
		_newURL := *_newReq.URL
		newReq.URL = &_newURL
		// 首先确定站点使用的通信协议
		if targets[0].URL.Scheme == "http" 
			newReq.URL.Scheme = "https"
		 else 
			newReq.URL.Scheme = "http"
		
		// 把配置好的 newReq 再添加到 targets,此时 Targets 有两个目标信息的元素
		targets = append(targets, newReq)
	
	// 把 Targets 添加到 crawlerTask/爬虫任务的字典
	crawlerTask.Targets = targets[:]

	// 设置请求对象的 source 为常量 "Target"
	for _, req := range targets 
		req.Source = config.FromTarget
	

	// 装配形式参数变量 Taskconfig
	// 设置 TaskConfig 即 taskConf 的变量,如果未设置则使用默认值
	设置 taskConf 的 变量
		.TabRunTimeout
		.MaxTabsCount
		.FilterMode
		.MaxCrawlCount
		.DomContentLoadedTimeout
		.EventTriggerInterval
		.BeforeExitDelay
		.EventTriggerMode
		.IgnoreKeywords

	// 设置请求头变量
	if taskConf.ExtraHeadersString != "" 
		err := json.Unmarshal([]byte(taskConf.ExtraHeadersString), &taskConf.ExtraHeaders)
		if err != nil 
			logger.Logger.Error("custom headers can't be Unmarshal.")
			return nil, err
		
	

	// 以无头模式开启 chrome.exe
	crawlerTask.Browser = engine2.InitBrowser(taskConf.ChromiumPath, taskConf.IncognitoContext, taskConf.ExtraHeaders, taskConf.Proxy, taskConf.NoHeadless)
	// 设置爬虫任务的根域名
	crawlerTask.RootDomain = targets[0].URL.RootDomain()

	// 初始化智能过滤器
	crawlerTask.smartFilter.Init()

	// 创建协程池
	// 创建协程池,并且把协程池保存到爬虫任务的 Pool 键
	p, _ := ants.NewPool(taskConf.MaxTabsCount)
	crawlerTask.Pool = p

	// 爬虫任务完成数据装配、无头模式启动浏览器、创建协程池
	return &crawlerTask, nil


启动爬虫任务

代码把请求对象添加到线程池,然后等待异步线程执行结束。

task_main.go 代码 210 行的函数 t.addTask2Pool(req),继续追溯。

	// crawlergo_cmd.go
	task.Run()

/*
开始当前任务
*/
func (t *CrawlerTask) Run() 
	defer t.Pool.Release()  // 释放协程池
	defer t.Browser.Close() // 关闭浏览器

	是否从 robots.txt 获取信息,如果是则把信息添加到 t.Targets 对象

	是否 Fuzz 目录,如果是则读取字典文件并添加到 t.Targets 对象

	// 创建数组对象 AllReqList
	t.Result.AllReqList = t.Targets[:]

	// 创建 Request 类型的初始化任务变量
	var initTasks []*model.Request
	for _, req := range t.Targets 
		if t.smartFilter.DoFilter(req) 
			logger.Logger.Debugf("filter req: " + req.URL.RequestURI())
			continue
		
		// 从 t.Targets 对象中获取请求对象
		initTasks = append(initTasks, req)
		t.Result.ReqList = append(t.Result.ReqList, req)
	
	logger.Logger.Info("filter repeat, target count: ", len(initTasks))

	// 遍历初始化任务数组的请求对象,逐个添加到协程池
	for _, req := range initTasks 
		if !engine2.IsIgnoredByKeywordMatch(*req, t.Config.IgnoreKeywords) 
			t.addTask2Pool(req)
		
	
	// 运行则发送请求
	// 等待异步线程执行完成
	t.taskWG.Wait()

	// 对全部请求进行唯一去重
	todoFilterAll := make([]*model.Request, len(t.Result.AllReqList))
	for index := range t.Result.AllReqList 
		todoFilterAll[index] = t.Result.AllReqList[index]
	

	t.Result.AllReqList = []*model.Request
	var simpleFilter filter2.SimpleFilter
	for _, req := range todoFilterAll 
		if !simpleFilter.UniqueFilter(req) 
			t.Result.AllReqList = append(t.Result.AllReqList, req)
		
	

	// 全部域名
	t.Result.AllDomainList = AllDomainCollect(t.Result.AllReqList)
	// 子域名
	t.Result.SubDomainList = SubDomainCollect(t.Result.AllReqList, t.RootDomain)

把任务添加到线程池

// task_main.go,t.addTask2Pool()位于 210 行
/*开始当前任务*/
func (t *CrawlerTask) Run() 
	……
	// 遍历初始化任务数组的请求对象,逐个添加到协程池
	for _, req := range initTasks 
		if !engine2.IsIgnoredByKeywordMatch(*req, t.Config.IgnoreKeywords) 
			t.addTask2Pool(req)
		
	


// 追溯 t.addTask2Pool(req),同文件代码 240 行
/**
添加任务到协程池
添加之前实时过滤
*/
func (t *CrawlerTask) addTask2Pool(req *model.Request) 

	// 协程相关代码
	t.taskCountLock.Lock()
	if t.crawledCount >= t.Config.MaxCrawlCount 
		t.taskCountLock.Unlock()
		return
	 else 
		t.crawledCount += 1
	
	t.taskCountLock.Unlock()

	t.taskWG.Add(1)

	// 传入请求对象(字典类型),追溯 t.generateTabTask(req) ,发现根据请求列表生成tabTask协程任务列表
	/* 实际上又嵌套了一层字典,字典结构如下所示
		task := tabTask
			crawlerTask: t,
			browser:     t.Browser,
			req:         req,
		
	*/
	task := t.generateTabTask(req)
	
	// 测试时F7单步调试,发现跳过了该部分代码
	// 查询资料发现,该部分代码作用是:以并发的方式调用匿名函数func
	go func() 
		// 提交协程任务执行
		err := t.Pool.Submit(task.Task)
		if err != nil 
			t.taskWG.Done()
			logger.Logger.Error("addTask2Pool ", err)
		
	()



// 追溯关键代码 t.Pool.submit(task.Task)

	在 t.Pool.submit(task.Task) 后添加打印提示信息的代码,发现打印了提示信息
	
	在该处下断点,进行单步调试
		发现使用F7或F8进行单步调试时,不会在该段代码的断点停留
		使用"运行到光标处",才会在该段代码的断点停留,
	
	然后就会执行 Run() 里面的代码:t.taskWG.Wait(),此时请求已经发送
	单步调试跟踪 t.taskWG.Wait(),发现的函数调用如下:
		task.Run()								crawlergo_cmd.go
		->t.taskWG.Wait()						task_main.go
			-> Wait()							src/sync/waitgroup.go
			->runtime_Semacquire(semap)			src/sync/waitgroup.go
				->sync_runtime_Semacquire()		src/runtime/sema.go
				F7步入执行该代码,此时会运行t.Pool.submit()并发送请求。
	也就是说,执行 t.taskWG.Wait()函数过程中,会执行 t.Pool.submit(),然后会执行协程任务。

	插个眼:注意func (t *tabTask) Task()

重要函数表格

创建爬虫任务

所属文件函数说明
crawlergo_cmd.gotask, err := pkg.NewCrawlerTask(targets, taskConfig)创建爬虫任务
task_main.goengine2.InitBrowser(xx)以无头模式启动浏览器 chrome.exe
-ants.NewPool(taskConf.MaxTabsCount)创建协程池

接着梳理协程任务的执行

关键代码:task_main.go 文件 代码 253 行。

go func() 
		// 每个 goroutine 调用 task.Task() 函数,目前不清楚为什么不是使用 task.Task(),可能是语法
		err := t.Pool.Submit(task.Task)
		fmt.Println(task.Task)
		fmt.Println("t.Pool.Submit")
		if err != nil 
			t.taskWG.Done()
			logger.Logger.Error("addTask2Pool ", err)
		
	()

启动 goroutine 执行操作

参考《Go in Action》第二章:一个 goroutine 是一个独立于其他函数运行的函数。使用关键字 go 启动一个 goroutine,并对这个 goroutine 做并发调度。代码使用关键字 go 启动了一个匿名函数作为 goroutine,这样可以并发地独立处理每个数据源的数据。

匿名函数内部包含了每个 goroutine 要完成的任务,此处是 t.Pool.Submit(task.Task)。

参考文章:ants:在Submit中再调用当前Pool的Submit可能导致阻塞,关于协程池 Pool。
goroutine pool减小开销的主要思路就是复用。即创建出的goroutine在做完一个task后不退出,而是等待下一个task,这样来减少goroutine反复创建和销毁带来的开销。

goroutine 的操作:task.Task

查看每个 goroutine 执行的操作,分析代码部分见注释内容。

// task_main.go 代码 264 行
/**
单个运行的tab标签任务,实现了workpool的接口
*/
func (t *tabTask) Task() 
	// 《Go in Action》第二章:
	毕设扫描器参数Fuzz第一篇:数据的定义读取和装配(爬虫数据和Payload数据)

攻防技术第二篇之-知己(防御手段)

攻防技术第二篇之-知己(防御手段)

毕设扫描器动态爬虫CrawlerGo源码分析1:cli库的使用

毕设扫描器动态爬虫CrawlerGo源码分析1:主文件cli库的使用

毕设扫描器动态爬虫CrawlerGo直观使用效果分析