不会飞的章鱼

熟能生巧,勤能补拙;念念不忘,必有回响。

Github开源项目crawlergo介绍

整体介绍

crawlergo是一个使用chrome headless模式进行URL收集的浏览器爬虫。它对整个网页的关键位置与DOM渲染阶段进行HOOK,自动进行表单填充并提交,配合智能的JS事件触发,
尽可能的收集网站暴露出的入口。内置URL去重模块,过滤掉了大量伪静态URL,对于大型网站仍保持较快的解析与抓取速度,最后得到高质量的请求结果集合。

crawlergo目前支持以下特性:

  • 原生浏览器环境,协程池调度任务
  • 表单智能填充、自动化提交
  • 完整DOM事件收集,自动化触发
  • 智能URL去重,去掉大部分的重复请求
  • 全面分析收集,包括javascript文件内容、页面注释、robots.txt文件和常见路径Fuzz
  • 支持Host绑定,自动添加Referer
  • 支持请求代理,支持爬虫结果主动推送

项目整体结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
.
├── [4.0K] cmd
│   └── [4.0K] crawlergo
│   ├── [7.4K] flag.go
│   └── [8.5K] main.go
├── [ 941] Disclaimer.md
├── [ 858] dockerfile
├── [4.0K] examples
│   ├── [1.2K] host_binding.py
│   ├── [1.2K] request_with_cookie.py
│   ├── [ 513] subprocess_call.py
│   └── [1.6K] zombie_clean.py
├── [ 690] get_chrome.sh
├── [ 588] go.mod
├── [9.6K] go.sum
├── [4.0K] imgs
│   ├── [ 36K] bypass.png
│   ├── [ 15K] chrome_path.png
│   ├── [1.1M] demo.gif
│   └── [220K] skp.png
├── [ 34K] LICENSE
├── [ 925] Makefile
├── [4.0K] pkg
│   ├── [4.0K] config
│   │   ├── [4.5K] config.go
│   │   └── [ 424] config_test.go
│   ├── [ 861] domain_collect.go
│   ├── [4.0K] engine
│   │   ├── [6.0K] after_dom_tasks.go
│   │   ├── [5.4K] after_loaded_tasks.go
│   │   ├── [2.8K] browser.go
│   │   ├── [2.0K] collect_links.go
│   │   ├── [8.8K] intercept_request.go
│   │   ├── [ 11K] tab.go
│   │   └── [ 525] tab_test.go
│   ├── [4.0K] filter
│   │   ├── [1.9K] simple_filter.go
│   │   ├── [ 20K] smart_filter.go
│   │   └── [1.7K] smart_filter_test.go
│   ├── [4.0K] js
│   │   └── [ 16K] javascript.go
│   ├── [4.0K] logger
│   │   └── [ 449] logger.go
│   ├── [4.0K] model
│   │   ├── [4.0K] request.go
│   │   ├── [3.6K] url.go
│   │   └── [2.0K] url_test.go
│   ├── [5.3K] path_expansion.go
│   ├── [5.3K] taskconfig.go
│   ├── [1.7K] taskconfig_test.go
│   ├── [7.5K] task_main.go
│   └── [4.0K] tools
│   ├── [1.4K] common.go
│   ├── [1.3K] random.go
│   └── [4.0K] requests
│   ├── [5.7K] requests.go
│   ├── [ 581] response.go
│   └── [ 510] utils.go
├── [ 11K] README.md
└── [9.6K] README_zh-cn.md

运行

首先确保已经安装了Go语言环境。

执行make build,会在当前目录下生成一个bin目录的文件夹。

假设你的chromium安装在 /tmp/chromium/ ,开启最大10标签页,爬取AWVS靶场:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ bin/crawlergo -c /home/neo/Downloads/chrome-linux/chrome -t 10 http://testphp.vulnweb.com/
INFO[0000] Init crawler task, host: testphp.vulnweb.com, max tab count: 10, max crawl count: 200.
INFO[0000] filter mode: smart
INFO[0000] If no matches, default form input text: Crawlergo
INFO[0000] Start crawling.
INFO[0000] filter repeat, target count: 2
INFO[0000] Crawling GET https://testphp.vulnweb.com/
INFO[0000] Crawling GET http://testphp.vulnweb.com/
WARN[0005] navigate timeout https://testphp.vulnweb.com/
INFO[0007] Crawling GET http://testphp.vulnweb.com/index.php
INFO[0007] Crawling GET http://testphp.vulnweb.com/artists.php
INFO[0007] Crawling GET http://testphp.vulnweb.com/AJAX/index.php
INFO[0007] Crawling GET http://testphp.vulnweb.com/guestbook.php
...

工作流程

初始化,声明作者,项目名称等信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
author := cli.Author{
Name: "9ian1i",
Email: "9ian1itp@gmail.com",
}

app := &cli.App{
Name: "crawlergo",
Usage: "A powerful browser crawler for web vulnerability scanners",
UsageText: "crawlergo [global options] url1 url2 url3 ... (must be same host)",
Version: Version,
Authors: []*cli.Author{&author},
Flags: cliFlags,
Action: run,
}

项目启动

1
2
3
4
5
6
err := app.Run(os.Args)
if err != nil {
logger.Logger.Fatal(err)
}

func run(c *cli.Context) error {...}

事件监听

使用channel来等待信号的到来,从而达到等待程序退出的目的。如果程序需要在接收到信号时进行一些特定的处理,可以在程序中添加对应的处理逻辑。

1
2
3
4
5
6
7
signalChan = make(chan os.Signal, 1)  // 创建一个带缓冲的channel signalChan
signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT) //注册3个系统信号SIGTERM、SIGQUIT、SIGINT。这些信号可以被操作系统或者其他程序发送给本程序,表示一些事件的发生,比如程序需要退出、用户按下了键盘上的 CTRL+C 等。
if c.Args().Len() == 0 {
// 如果在运行程序时未设置 url ,则会记录日志并返回一个错误,提示用户必须设置 url。
logger.Logger.Error("url must be set")
return errors.New("url must be set")
}

设置日志输出级别

1
2
3
4
5
level, err := logrus.ParseLevel(logLevel)
if err != nil {
logger.Logger.Fatal(err)
}
logger.Logger.SetLevel(level)

检查自定义的表单参数配置

1
2
3
4
5
6
7
8
taskConfig.CustomFormValues, err = parseCustomFormValues(customFormTypeValues.Value())
if err != nil {
logger.Logger.Fatal(err)
}
taskConfig.CustomFormKeywordValues, err = keywordStringToMap(customFormKeywordValues.Value())
if err != nil {
logger.Logger.Fatal(err)
}

开始爬虫任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
task, err := pkg.NewCrawlerTask(targets, taskConfig)  // 新建爬虫任务
if err != nil {
logger.Logger.Error("create crawler task failed.")
os.Exit(-1)
}
if len(targets) != 0 {
logger.Logger.Info(fmt.Sprintf("Init crawler task, host: %s, max tab count: %d, max crawl count: %d.",
targets[0].URL.Host, taskConfig.MaxTabsCount, taskConfig.MaxCrawlCount))
logger.Logger.Info("filter mode: ", taskConfig.FilterMode)
}

// 提示自定义表单填充参数
if len(taskConfig.CustomFormValues) > 0 {
logger.Logger.Info("Custom form values, " + tools.MapStringFormat(taskConfig.CustomFormValues))
}
// 提示自定义表单填充参数
if len(taskConfig.CustomFormKeywordValues) > 0 {
logger.Logger.Info("Custom form keyword values, " + tools.MapStringFormat(taskConfig.CustomFormKeywordValues))
}
if _, ok := taskConfig.CustomFormValues["default"]; !ok {
logger.Logger.Info("If no matches, default form input text: " + config.DefaultInputText)
taskConfig.CustomFormValues["default"] = config.DefaultInputText
}

go handleExit(task) // 等待系统信号,并在接收到信号时关闭程序
logger.Logger.Info("Start crawling.")
task.Run() // 核心逻辑
result := task.Result

// 内置请求代理
if pushAddress != "" {
logger.Logger.Info("pushing results to ", pushAddress, ", max pool number:", pushProxyPoolMax)
Push2Proxy(result.ReqList)
}

结果输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func outputResult(result *pkg.Result) {
// 输出结果
if outputMode == "json" {
// 将 result 对象以 JSON 格式序列化并输出
fmt.Println("--[Mission Complete]--")
resBytes := getJsonSerialize(result)
fmt.Println(string(resBytes))
} else if outputMode == "console" {
// 逐个打印 result 中抓取到的所有请求和它们的响应
for _, req := range result.ReqList {
req.FormatPrint()
}
}
if len(outputJsonPath) != 0 {
// 如果在命令行输入了可选参数 outputJsonPath ,那么将结果以 JSON 格式序列化并保存到指定的文件中。
resBytes := getJsonSerialize(result)
tools.WriteFile(outputJsonPath, resBytes)
}
}
------ 本文结束------
如果本篇文章对你有帮助,可以给作者加个鸡腿~(*^__^*),感谢鼓励与支持!