Skip to content

Latest commit

 

History

History
1132 lines (866 loc) · 55.9 KB

10.md

File metadata and controls

1132 lines (866 loc) · 55.9 KB

十、爬虫

从网络上收集信息在许多情况下都很有用。网站可以提供丰富的信息。这些信息可用于在执行社会工程攻击或网络钓鱼攻击时提供帮助。您可以查找潜在目标的名称和电子邮件,或收集关键字和标题,以帮助快速了解网站的主题或业务。您还可以潜在地了解企业的位置,查找图像和文档,并使用 web 抓取技术分析网站的其他方面。

了解目标可以让你创造一个可信的借口。借口是攻击者常用的一种技术,用于诱使毫无戒备的受害者遵守以某种方式损害用户、其帐户或其机器的请求。例如,有人研究了一家公司,发现它是一家大型公司,在特定城市有一个集中的 it 支持部门。他们可以假装是技术支持人员,给公司的人打电话或发电子邮件,要求他们执行操作或提供密码。来自公司公共网站的信息可能包含许多用于设置借口情况的细节。

Web 爬行是抓取的另一个方面,它涉及到跟踪指向其他页面的超链接。广度优先爬网指的是找到尽可能多的不同网站,并跟踪它们以找到更多的网站。深度优先爬网是指在移动到下一个站点之前,对单个站点进行爬网以查找所有可能的页面。

在本章中,我们将介绍 web 抓取和 web 爬行。我们将向您介绍一些基本任务的示例,如查找链接、文档和图像、查找隐藏的文件和信息,以及使用名为goquery的强大第三方软件包。我们还将讨论减少对您自己的网站的刮擦的技术。

在本章中,我们将具体介绍以下主题:

  • 爬虫基础
    • 字符串匹配
    • 正则表达式
    • 从响应中提取 HTTP 头
    • 使用 cookies
    • 从页面中提取 HTML 注释
    • 在 web 服务器上搜索未列出的文件
    • 修改您的用户代理
    • 对 web 应用和服务器进行指纹识别
  • 使用 goquery 包
    • 列出页面中的所有链接
    • 列出页面中的所有文档链接
    • 列出页面的标题和标题
    • 计算页面上最常用的单词
    • Listing all external JavaScript sources of a page
    • 深度优先爬行
    • 广度优先爬行
  • 防止网页刮花

爬虫基础

本书中使用的 Web scraping 是从 HTML 结构化页面中提取信息的过程,该页面旨在供用户查看,而不是以编程方式使用。有些服务提供了一个 API,可以高效地进行编程使用,但有些网站只提供 HTML 页面中的信息。这些 web 抓取示例演示了从 HTML 中提取信息的各种方法。我们将研究基本的字符串匹配,然后是正则表达式,然后是一个强大的用于 web 抓取的名为goquery的包。

使用 strings 包在 HTTP 响应中查找字符串

首先,让我们看看如何发出一个基本的 HTTP 请求并使用标准库搜索字符串。首先,我们将创建http.Client并设置任何自定义变量;例如,客户端是否应遵循重定向、应使用哪组 cookie 或使用哪种传输。

The http.Transport type implements the network request operations to perform the HTTP request and get a response. By default, http.RoundTripper is used, and this executes a single HTTP request. For the majority of use cases, the default transport is just fine. By default, the HTTP proxy from the environment is used, but the proxy can also be specified in the transport. This might be useful if you want to use multiple proxies. This example does not use a custom http.Transport type, but I wanted to highlight how http.Transport is an embedded type within http.Client.

我们正在创建一个自定义的http.Client类型,但只是为了覆盖Timeout字段。默认情况下,没有超时,应用可能永远挂起。

http.Client中可以重写的另一个嵌入式类型是http.CookieJar类型。http.CookieJar接口需要两个功能:SetCookies()Cookies()。标准库附带了net/http/cookiejar包,其中包含CookieJar的默认实现。多个 cookie jar 的一个用例是登录并存储一个网站的多个会话。您可以登录任意多个用户,并将每个会话存储在 cookie jar 中,并根据需要使用每个会话。此示例不使用自定义 cookie jar。

HTTP 响应包含作为读卡器接口的主体。我们可以使用任何接受读卡器接口的函数从读卡器中提取数据。这包括诸如io.Copy()io.ReadAtLeast()io.ReadlAll()bufio缓冲读卡器等功能。在本例中,ioutil.ReadAll()用于将 HTTP 响应的完整内容快速存储到字节片变量中。

以下是此示例的代码实现:

// Perform an HTTP request to load a page and search for a string
package main

import (
   "fmt"
   "io/ioutil"
   "log"
   "net/http"
   "os"
   "strings"
   "time"
)

func main() {
   // Load command line arguments
   if len(os.Args) != 3 {
      fmt.Println("Search for a keyword in the contents of a URL")
      fmt.Println("Usage: " + os.Args[0] + " <url> <keyword>")
      fmt.Println("Example: " + os.Args[0] + 
         " https://www.devdungeon.com NanoDano")
      os.Exit(1)
   }
   url := os.Args[1]
   needle := os.Args[2] // Like searching for a needle in a haystack

   // Create a custom http client to override default settings. Optional
   // Use http.Get() instead of client.Get() to use default client.
   client := &http.Client{
      Timeout: 30 * time.Second, // Default is forever!
      // CheckRedirect - Policy for following HTTP redirects
      // Jar - Cookie jar holding cookies
      // Transport - Change default method for making request
   }

   response, err := client.Get(url)
   if err != nil {
      log.Fatal("Error fetching URL. ", err)
   }

   // Read response body
   body, err := ioutil.ReadAll(response.Body)
   if err != nil {
      log.Fatal("Error reading HTTP body. ", err)
   }

   // Search for string
   if strings.Contains(string(body), needle) {
      fmt.Println("Match found for " + needle + " in URL " + url)
   } else {
      fmt.Println("No match found for " + needle + " in URL " + url)
   }
} 

使用正则表达式查找页面中的电子邮件地址

正则表达式或正则表达式本身实际上是一种语言形式。本质上,它是一个表示文本搜索模式的特殊字符串。使用 shell 时,您可能熟悉星号(*。像ls *.txt这样的命令使用一个简单的正则表达式。本例中的星号表示任何;因此,任何字符串都将匹配,只要它以.txt结尾。除星号外,正则表达式还有其他符号,如句点(.),它匹配任何单个字符,而星号则匹配任何长度的字符串。甚至还有更强大的表达式,可以用一些可用的符号来制作。

正则表达式以速度慢著称。基于输入长度,所使用的实现保证在线性时间内运行,而不是在指数时间内运行。这意味着它将比许多其他不提供这种保证的正则表达式实现(如 Perl)运行得更快。Go 的作者之一 Russ Cox 在 2007 年发表了两种不同方法的深入比较,可在上查阅 https://swtch.com/~rsc/regexp/regexp1.html。这对于搜索 HTML 页面内容的用例非常重要。如果正则表达式以指数时间运行(基于输入长度),则执行某些表达式的搜索可能需要相当长的时间。

了解有关正则表达式的更多信息 https://en.wikipedia.org/wiki/Regular_expression处的相关 Go 文件 https://golang.org/pkg/regexp/

本例使用正则表达式搜索嵌入 HTML 中的电子邮件地址链接。它将搜索任何mailto链接并提取电子邮件地址。我们将使用默认 HTTP 客户端并调用http.Get(),而不是创建自定义客户端来修改超时。

典型的电子邮件链接如下所示:

<a href="mailto:nanodano@devdungeon.com">
<a href="mailto:nanodano@devdungeon.com?subject=Hello">

本例中使用的正则表达式如下:

"mailto:.*?["?]

让我们将其分解并检查每个部分:

  • "mailto::这整段文字只是一个字符串文字。第一个字符是引号("),在正则表达式中没有特殊含义。它被视为一个常规字符。这意味着正则表达式将首先搜索引号字符。引号后面是带冒号的文本mailto:。冒号也没有特殊的含义。

  • .*?:句点(.表示匹配除换行符以外的任何字符。星号表示根据上一个符号(句点)继续匹配零个或多个字符。星号后面是一个问号(?)。这个问号告诉星号是非贪婪的。它将匹配尽可能短的字符串。如果没有它,星号将继续尽可能长的匹配,同时仍然满足完整的正则表达式。我们只需要电子邮件地址本身,而不需要任何查询参数,如?subject,因此我们告诉它进行非贪婪或短匹配。

  • ["?]:正则表达式的最后一块是["?]集。括号告诉正则表达式匹配由括号封装的任何字符。我们只有两个字符:引号和问号。这里的问号没有特殊意义,被视为一个规则字符。括号内的两个字符是删除电子邮件地址结尾的两个可能字符。默认情况下,正则表达式将使用最后一个,并返回尽可能长的字符串,因为它前面的星号是贪婪的。但是,由于我们在上一节中直接在星号后面添加了另一个问号,因此它将执行非贪婪搜索,并在第一个匹配括号内字符的地方停止。

使用这种技术意味着我们只能在 HTML 中找到使用<a>标记显式链接的电子邮件。它不会在页面中找到仅以明文形式书写的电子邮件。创建正则表达式以基于模式(如<word>@<word>.<word>)搜索电子邮件字符串可能看起来很简单,但不同正则表达式实现之间的细微差别以及电子邮件可能具有的复杂变化使得很难创建捕获所有有效电子邮件组合的正则表达式。如果你在网上快速搜索一个例子,你会看到有多少变化,它们有多复杂。

如果您正在创建某种 web 服务,那么通过向某人发送电子邮件并让他们以某种方式回复或验证链接来验证其电子邮件帐户是很重要的。我不建议您仅依靠正则表达式来确定电子邮件是否有效,我还建议您在使用正则表达式执行客户端电子邮件验证时要格外小心。用户可能有一个技术上有效的奇怪电子邮件地址,您可能会阻止他们注册您的服务。

以下是一些根据 1982 年的RFC 822实际有效的电子邮件地址示例:

  • *.*@example.com
  • $what^the.#!$%@example.com
  • !#$%^&*=()@example.com
  • "!@#$%{}^&~*()|/="@example.com
  • "hello@example.com"@example.com

2001 年,RFC 2822取代RFC 822。在前面的所有示例中,只有最后两个包含 at(@符号的示例被较新的RFC 2822视为无效。所有其他例子仍然有效。阅读上的原始 RFChttps://www.ietf.org/rfc/rfc822.txthttps://www.ietf.org/rfc/rfc2822.txt

The following is the code implementation of this example:

// Search through a URL and find mailto links with email addresses
package main

import (
   "fmt"
   "io/ioutil"
   "log"
   "net/http"
   "os"
   "regexp"
)

func main() {
   // Load command line arguments
   if len(os.Args) != 2 {
      fmt.Println("Search for emails in a URL")
      fmt.Println("Usage: " + os.Args[0] + " <url>")
      fmt.Println("Example: " + os.Args[0] + 
         " https://www.devdungeon.com")
      os.Exit(1)
   }
   url := os.Args[1]

   // Fetch the URL
   response, err := http.Get(url)
   if err != nil {
      log.Fatal("Error fetching URL. ", err)
   }

   // Read the response
   body, err := ioutil.ReadAll(response.Body)
   if err != nil {
      log.Fatal("Error reading HTTP body. ", err)
   }

   // Look for mailto: links using a regular expression
   re := regexp.MustCompile("\"mailto:.*?[?\"]")
   matches := re.FindAllString(string(body), -1)
   if matches == nil {
      // Clean exit if no matches found
      fmt.Println("No emails found.")
      os.Exit(0)
   }

   // Print all emails found
   for _, match := range matches {
      // Remove "mailto prefix and the trailing quote or question mark
      // by performing a slice operation to extract the substring
      cleanedMatch := match[8 : len(match)-1]
      fmt.Println(cleanedMatch)
   }
} 

从 HTTP 响应中提取 HTTP 头

HTTP 头包含有关请求和响应的元数据和描述性信息。通过检查服务器与响应一起提供的 HTTP 头,您可能会了解到很多关于服务器的信息。您可以了解有关服务器的以下内容:

  • 缓存系统
  • 认证
  • 操作系统
  • 网络服务器
  • 响应类型
  • 框架或内容管理系统
  • 程序设计语言
  • 口语
  • 安全标头
  • 曲奇饼

并不是每个 web 服务器都会返回所有这些头文件,但是尽可能多地从这些头文件中学习是很有帮助的。WordPress 和 Drupal 等流行框架将返回一个X-Powered-By标题,告诉您是 WordPress 还是 Drupal 以及它的版本。

会话 cookie 也会泄露大量信息。一个名为PHPSESSID的 cookie 告诉您它很可能是一个 PHP 应用。Django 的默认会话 cookie 命名为sessionid,Java 的默认会话 cookie 命名为JSESSIONID,Ruby on Rail 的会话 cookie 遵循_APPNAME_session模式。您可以使用这些线索对 web 服务器进行指纹识别。如果您只需要标题,而不需要整个页面,那么您可以始终使用 HTTPHEAD方法而不是 HTTPGETHEAD方法将只返回标题。

本例向 URL 发出HEAD请求并打印出其所有标题。http.Response类型包含字符串到名为Header的字符串的映射,其中包含每个 HTTP 头的键值对:

// Perform an HTTP HEAD request on a URL and print out headers
package main

import (
   "fmt"
   "log"
   "net/http"
   "os"
)

func main() {
   // Load URL from command line arguments
   if len(os.Args) != 2 {
      fmt.Println(os.Args[0] + " - Perform an HTTP HEAD request to a URL")
      fmt.Println("Usage: " + os.Args[0] + " <url>")
      fmt.Println("Example: " + os.Args[0] + 
         " https://www.devdungeon.com")
      os.Exit(1)
   }
   url := os.Args[1]

   // Perform HTTP HEAD
   response, err := http.Head(url)
   if err != nil {
      log.Fatal("Error fetching URL. ", err)
   }

   // Print out each header key and value pair
   for key, value := range response.Header {
      fmt.Printf("%s: %s\n", key, value[0])
   }
} 

使用 HTTP 客户端设置 Cookie

Cookie 是现代 web 应用的基本组件。Cookie 作为 HTTP 头在客户端和服务器之间来回发送。Cookie 只是由浏览器客户端存储的文本键值对。它们用于在客户端上存储持久数据。它们可用于存储任何文本值,但通常用于存储首选项、令牌和会话信息。

会话 cookie 通常存储与服务器拥有的令牌匹配的令牌。当用户登录时,服务器使用绑定到该用户的标识令牌创建会话。然后,服务器以 cookie 的形式将令牌发送回用户。当客户端以 cookie 的形式发送会话令牌时,服务器会在会话存储中查找并找到匹配的令牌,该会话存储可以是数据库、文件或内存。会话令牌需要足够的熵来确保它是唯一的,攻击者无法猜测它。

如果用户在公共 Wi-Fi 网络上并访问不使用 SSL 的网站,附近的任何人都可以看到明文形式的 HTTP 请求。攻击者可以窃取会话 cookie 并在自己的请求中使用它。当 cookie 以这种方式被侧劫持时,攻击者可以模拟受害者。服务器会将他们视为已登录的用户。攻击者可能永远不会知道密码,也不需要知道。

For this reason, it can be useful to log out of websites occasionally and destroy any active sessions. Some websites allow you to manually destroy all active sessions. If you run a web service, I recommend that you set a reasonable expiration time for sessions. Bank websites do a good job of this usually enforcing a short 10-15 minute expiration.

创建新 cookie 时,服务器会向客户端发送一个Set-Cookie头。然后,客户端使用Cookie头将 cookie 发送回服务器。

以下是从服务器发送的 cookie 头的简单示例:

Set-Cookie: preferred_background=blue
Set-Cookie: session_id=PZRNVYAMDFECHBGDSSRLH

以下是来自客户端的示例标头:

Cookie: preferred_background=blue; session_id=PZRNVYAMDFECHBGDSSRLH

There are other attributes that a cookie can contain, such as the Secure and HttpOnly flags discussed in Chapter 9, Web Applications. Other attributes include an expiration date, a domain, and a path. This example is only presenting the simplest application.

在本例中,使用自定义会话 cookie 发出简单请求。会话 cookie 允许您在向网站发出请求时登录。本示例应作为如何使用 cookie 而不是独立工具发出请求的参考。首先,URL 是在main函数之前定义的。然后,首先使用指定的 HTTPGET方法创建 HTTP 请求。由于GET请求通常不需要正文,因此提供了 nil 正文。然后用一个新的头 cookie 更新新的请求。在本例中,session_id是会话 cookie 的名称,但这取决于与之交互的 web 应用。

一旦准备好请求,就会创建一个 HTTP 客户机来实际发出请求并处理响应。请注意,HTTP 请求和 HTTP 客户端是独立的实体。例如,您可以多次重用一个请求,对不同的客户端使用一个请求,对单个客户端使用多个请求。如果需要管理多个客户端会话,这允许您使用不同的会话 cookie 创建多个请求对象。

以下是此示例的代码实现:

package main

import (
   "fmt"
   "io/ioutil"
   "log"
   "net/http"
)

var url = "https://www.example.com"

func main() {
   // Create the HTTP request
   request, err := http.NewRequest("GET", url, nil)
   if err != nil {
      log.Fatal("Error creating HTTP request. ", err)
   }

   // Set cookie
   request.Header.Set("Cookie", "session_id=<SESSION_TOKEN>")

   // Create the HTTP client, make request and print response
   httpClient := &http.Client{}
   response, err := httpClient.Do(request)
   data, err := ioutil.ReadAll(response.Body)
   fmt.Printf("%s\n", data)
} 

在网页中查找 HTML 注释

HTML 注释有时可以保存惊人的信息片段。我个人曾在 HTML 评论中看到过使用管理员用户名和密码的网站。我还看到一个完整的菜单被注释掉了,但是链接仍然有效,可以直接访问。您永远不知道粗心的开发人员可能会留下什么样的信息。

如果要在代码中留下注释,最好将注释留在服务器端代码中,而不是面向客户端的 HTML 和 JavaScript 中。用 PHP、Ruby、Python 或任何后端代码进行注释。您永远不想给客户提供比他们在代码中需要的更多的信息。

The regular expression used in this program consists of a few special sequences. Here is the full regular expression. It essentially says, "match anything between the <!-- and --> strings." Let's examine it piece by piece:

  • <!--(.|\n)*?-->:开头和结尾以<!---->开头,这是打开和关闭 HTML 注释的名称。这些是普通字符,而不是正则表达式的特殊字符。

  • (.|\n)*?:可分为两部分:

试着在一些网站上运行这个程序,看看你能找到什么样的 HTML 注释。你可能会惊讶于你能发现什么样的信息。例如,MailChimp 注册表单附带了一个 HTML 注释,它实际上为您提供了绕过 bot 注册预防的提示。MailChimp 注册表单使用了一个不应填写的蜜罐字段,或者它假设表单是由机器人提交的。看看你能找到什么。

本例将首先获取提供的 URL,然后使用前面介绍的正则表达式搜索 HTML 注释。然后将找到的每个匹配项打印到标准输出:

// Search through a URL and find HTML comments
package main

import (
   "fmt"
   "io/ioutil"
   "log"
   "net/http"
   "os"
   "regexp"
)

func main() {
   // Load command line arguments
   if len(os.Args) != 2 {
      fmt.Println("Search for HTML comments in a URL")
      fmt.Println("Usage: " + os.Args[0] + " <url>")
      fmt.Println("Example: " + os.Args[0] + 
         " https://www.devdungeon.com")
      os.Exit(1)
   }
   url := os.Args[1]

   // Fetch the URL and get response
   response, err := http.Get(url)
   if err != nil {
      log.Fatal("Error fetching URL. ", err)
   }
   body, err := ioutil.ReadAll(response.Body)
   if err != nil {
      log.Fatal("Error reading HTTP body. ", err)
   }

   // Look for HTML comments using a regular expression
   re := regexp.MustCompile("<!--(.|\n)*?-->")
   matches := re.FindAllString(string(body), -1)
   if matches == nil {
      // Clean exit if no matches found
      fmt.Println("No HTML comments found.")
      os.Exit(0)
   }

   // Print all HTML comments found
   for _, match := range matches {
      fmt.Println(match)
   }
} 

在 web 服务器上查找未列出的文件

有一个叫做 DirBuster 的流行程序,渗透测试人员使用该程序查找未列出的文件。DirBuster 是一个 OWASP 项目,预装在流行的渗透测试 Linux 发行版 Kali 上。只需使用标准库,我们就可以创建一个快速、并发且简单的 DirBuster 克隆,只需几行代码。有关 DirBuster 的更多信息,请访问https://www.owasp.org/index.php/Category:OWASP_DirBuster_Project

这个程序是 DirBuster 的一个简单克隆,它根据单词列表搜索未列出的文件。您必须创建自己的单词列表。这里将提供一个示例文件名的小列表,为您提供一些想法并用作起始列表。根据自己的经验和源代码构建文件列表。某些 web 应用具有特定名称的文件,允许您确定正在使用哪个框架。还要查找备份文件、配置文件、版本控制文件、更改日志文件、私钥、应用日志以及其他不打算公开的文件。你也可以在互联网上找到预先构建的单词列表,包括 DirBuster 的列表。

以下是您可以搜索的文件示例列表:

  • .gitignore
  • .git/HEAD
  • id_rsa
  • debug.log
  • database.sql
  • index-old.html
  • backup.zip
  • config.ini
  • settings.ini
  • settings.php.bak
  • CHANGELOG.txt

This program will search a domain with the provided word list and report any files that do not return a 404 NOT FOUND response. The word list should have filenames separated with a newline and have one filename per line. When providing the domain name as a parameter, the trailing slash is optional, and the program will behave properly with or without the trailing slash on the domain name. The protocol must be specified though, so that the request knows whether to use HTTP or HTTPS.

url.Parse()函数用于创建正确的 URL 对象。通过 URL 类型,您可以独立修改Path而无需修改HostScheme。这提供了一种简单的方法来更新 URL,而无需手动操作字符串。

要逐行读取文件,需要使用扫描仪。默认情况下,扫描程序按换行符拆分,但可以通过调用scanner.Split()并提供自定义拆分函数来覆盖它们。我们使用默认行为,因为这些词应该在单独的行中提供:

// Look for unlisted files on a domain
package main

import (
   "bufio"
   "fmt"
   "log"
   "net/http"
   "net/url"
   "os"
   "strconv"
)

// Given a base URL (protocol+hostname) and a filepath (relative URL)
// perform an HTTP HEAD and see if the path exists.
// If the path returns a 200 OK print out the path
func checkIfUrlExists(baseUrl, filePath string, doneChannel chan bool) {
   // Create URL object from raw string
   targetUrl, err := url.Parse(baseUrl)
   if err != nil {
      log.Println("Error parsing base URL. ", err)
   }
   // Set the part of the URL after the host name
   targetUrl.Path = filePath

   // Perform a HEAD only, checking status without
   // downloading the entire file
   response, err := http.Head(targetUrl.String())
   if err != nil {
      log.Println("Error fetching ", targetUrl.String())
   }

   // If server returns 200 OK file can be downloaded
   if response.StatusCode == 200 {
      log.Println(targetUrl.String())
   }

   // Signal completion so next thread can start
   doneChannel <- true
}

func main() {
   // Load command line arguments
   if len(os.Args) != 4 {
      fmt.Println(os.Args[0] + " - Perform an HTTP HEAD request to a URL")
      fmt.Println("Usage: " + os.Args[0] + 
         " <wordlist_file> <url> <maxThreads>")
      fmt.Println("Example: " + os.Args[0] + 
         " wordlist.txt https://www.devdungeon.com 10")
      os.Exit(1)
   }
   wordlistFilename := os.Args[1]
   baseUrl := os.Args[2]
   maxThreads, err := strconv.Atoi(os.Args[3])
   if err != nil {
      log.Fatal("Error converting maxThread value to integer. ", err)
   }

   // Track how many threads are active to avoid
   // flooding a web server
   activeThreads := 0
   doneChannel := make(chan bool)

   // Open word list file for reading
   wordlistFile, err := os.Open(wordlistFilename)
   if err != nil {
      log.Fatal("Error opening wordlist file. ", err)
   }

   // Read each line and do an HTTP HEAD
   scanner := bufio.NewScanner(wordlistFile)
   for scanner.Scan() {
      go checkIfUrlExists(baseUrl, scanner.Text(), doneChannel)
      activeThreads++

      // Wait until a done signal before next if max threads reached
      if activeThreads >= maxThreads {
         <-doneChannel
         activeThreads -= 1
      }
   }

   // Wait for all threads before repeating and fetching a new batch
   for activeThreads > 0 {
      <-doneChannel
      activeThreads -= 1
   }

   // Scanner errors must be checked manually
   if err := scanner.Err(); err != nil {
      log.Fatal("Error reading wordlist file. ", err)
   }
} 

更改请求的用户代理

A common technique to block scrapers and crawlers is to block certain user agents. Some services will blacklist certain user agents that contain keywords such as curl and python. You can get around most of these by simply changing your user agent to firefox.

To set the user agent, you must first create the HTTP request object. The header must be set before making the actual request. This means that you can't use the shortcut convenience functions such as http.Get(). We have to create the client and then create a request, and then use the client to client.Do() the request.

本例使用http.NewRequest()创建一个 HTTP 请求,然后修改请求头以覆盖User-Agent头。你可以用它来隐藏、伪造或诚实。为了成为一个好的网络公民,我建议你为你的爬虫程序创建一个独特的用户代理,这样网站管理员就可以阻止你的机器人。我还建议您在用户代理中包含一个网站或电子邮件地址,以便网站管理员可以请求您的用户跳过。

以下是此示例的代码实现:

// Change HTTP user agent
package main

import (
   "log"
   "net/http"
)

func main() {
   // Create the request for use later
   client := &http.Client{}
   request, err := http.NewRequest("GET", 
      "https://www.devdungeon.com", nil)
   if err != nil {
      log.Fatal("Error creating request. ", err)
   }

   // Override the user agent
   request.Header.Set("User-Agent", "_Custom User Agent_")

   // Perform the request, ignore response.
   _, err = client.Do(request)
   if err != nil {
      log.Fatal("Error making request. ", err)
   }
} 

指纹 web 应用技术栈

对 web 应用进行指纹识别是指您试图识别用于为 web 应用提供服务的技术。指纹识别可以在几个层次上进行。在较低的级别上,HTTP 头可以提供关于运行什么操作系统(如 Windows 或 Linux)和什么 web 服务器(如 Apache 或 nginx)的线索。标题还可以提供有关在应用级别使用的编程语言或框架的信息。在更高的层次上,可以对 web 应用进行指纹识别,以确定正在使用哪些 JavaScript 库、是否包含任何分析平台、是否显示任何广告网络、正在使用的缓存层以及其他信息。我们将首先查看 HTTP 头,然后介绍更复杂的指纹识别方法。

指纹识别是攻击或渗透测试中的一个关键步骤,因为它有助于缩小选项范围并确定采取哪些路径。识别正在使用的技术还可以搜索已知的漏洞。如果 web 应用没有保持最新,那么查找和利用已知漏洞可能只需要简单的指纹和漏洞搜索。如果没有别的,它会帮助你了解目标。

基于 HTTP 响应头的指纹识别

我建议您首先检查 HTTP 头,因为它们是简单的键值对,并且通常每个请求只返回几个。手动检查头文件不需要很长时间,因此您可以先检查它们,然后再转到应用。应用级别的指纹识别更为复杂,我们稍后将讨论这一点。在本章的前面,有一节是关于提取 HTTP 头并打印出来供检查的(从 HTTP 响应中提取 HTTP 头的节)。您可以使用该程序转储不同网页的标题,并查看您可以找到什么。

基本思想很简单。寻找关键词。某些标题尤其包含最明显的线索,例如X-Powered-ByServerX-Generator标题。X-Powered-By头可以包含正在使用的框架或内容管理系统CMS)的名称,如 WordPress 或 Drupal。

检查标题有两个基本步骤。首先,您需要获取标题。使用本章前面提供的示例提取 HTTP 头。第二步是进行字符串搜索以查找关键字。您可以使用strings.ToUpper()strings.Contains()直接搜索关键字,也可以使用正则表达式。请参阅本章前面解释如何使用正则表达式的示例。一旦您能够搜索标题,您只需要能够生成要搜索的关键字列表。

您可以查找许多关键字。你要找什么取决于你要找什么。我将尝试涵盖几个大的类别,为您提供关于寻找什么的想法。您可以尝试识别的第一件事是主机正在运行的操作系统。以下是可在 HTTP 标头中找到的用于指示操作系统的关键字示例列表:

  • Linux
  • Debian
  • Fedora
  • Red Hat
  • CentOS
  • Ubuntu
  • FreeBSD
  • Win32
  • Win64
  • Darwin

下面是一个关键字列表,可以帮助您确定正在使用哪个 web 服务器。这并不是一个详尽的列表,但包含了几个关键字,如果您搜索互联网,这些关键字将产生结果:

  • Apache
  • Nginx
  • Microsoft-IIS
  • Tomcat
  • WEBrick
  • Lighttpd
  • IBM HTTP Server

确定正在使用哪种编程语言可以使您的攻击选择有很大的不同。与 Java 服务器或 ASP.NET 应用相比,PHP 等脚本语言容易受到不同的攻击。以下是几个示例关键字,您可以使用这些关键字在 HTTP 头中搜索,以确定为应用供电的语言:

  • Python
  • Ruby
  • Perl
  • PHP
  • ASP.NET

会话 cookie 也是关于正在使用什么框架或语言的重要赠品。例如,PHPSESSID表示 PHP,JSESSIONID表示 Java。以下是一些您可以搜索的会话 cookie:

  • PHPSESSID
  • JSESSIONID
  • session
  • sessionid
  • CFID/CFTOKEN
  • ASP.NET_SessionId

指纹识别 web 应用

一般来说,指纹识别 web 应用所涵盖的范围比只查看 HTTP 头要广得多。您可以在 HTTP 头中进行基本的关键字搜索,正如刚才所讨论的,并且可以学到很多东西,但是 HTML 源代码和服务器上其他文件的内容(或者仅仅是存在)中也有丰富的信息。

在 HTML 源代码中,您可以查找一些线索,例如页面本身的结构、类的名称和 HTML 元素的 ID。AngularJS 应用具有不同的 HTML 属性,例如ng-app,可以用作指纹识别的关键字。Angular 通常也包含在一个script标记中,与包含其他框架(如 jQuery)的方式相同。script标签还可以检查其他线索。寻找谷歌分析、AdSense、雅虎广告、Facebook、Discus、Twitter 和其他嵌入的第三方 JavaScript。

只需查看 URL 中的文件扩展名就可以告诉您正在使用什么语言。例如,.php.jsp.asp分别表示正在使用 PHP、Java 和 ASP。

We also looked at a program that finds HTML comments in a web page. Some frameworks and CMSes leave an identifiable footer or hidden HTML comment. Sometimes the marker is in the form of a small image.

目录结构也可以是另一个赠品。首先需要熟悉不同的框架。例如,Drupal 将站点信息存储在名为/sites/default的目录中。如果您试图访问该 URL,并且得到 403 禁止响应,而不是 404 未找到错误,那么您可能找到了一个基于 Drupal 的网站。

查找诸如wp-cron.php之类的文件。在在 web 服务器上查找未列出的文件部分中,我们研究了使用 DirBuster 克隆查找未列出的文件。查找可用于对 web 应用进行指纹识别的唯一文件列表,并将其添加到 word 列表中。您可以通过检查不同 web 框架的代码库来确定要查找哪些文件。例如,WordPress 和 Drupal 的源代码是公开的。使用本章前面讨论的程序查找未列出的文件以搜索文件。您可以搜索的其他未列出的文件与文档相关,例如CHANGELOG.txtreadme.txtreadme.mdreadme.htmlLICENSE.txtinstall.txtinstall.php

通过对正在运行的应用的版本进行指纹识别,可以从 web 应用中获得更多细节。如果您可以访问源代码,这会容易得多。我将使用 WordPress 作为一个例子,因为它是如此普遍,并且源代码可以在 GitHub 的上找到 https://github.com/WordPress/WordPress

目标是找出不同版本之间的差异。WordPress 是一个很好的例子,因为它们都带有包含所有管理接口的/wp-admin/目录。在/wp-admin/中,有cssjs文件夹,其中分别包含样式表和脚本。当站点托管在服务器上时,这些文件可以公开访问。在这些文件夹上使用diff命令来识别哪些版本引入新文件、哪些版本删除文件以及哪些版本修改现有文件。结合所有这些信息,您通常可以将应用缩小到特定版本或至少小范围的版本。

作为一个人为的例子,假设版本 1.0 只包含一个文件:main.js。版本 1.1 引入了第二个文件:utility.js。1.3 版删除了这两个文件,并将其替换为一个文件:master.js。您可以对以下三个文件向 web 服务器发出 HTTP 请求:main.jsutility.jsmaster.js。根据发现哪些文件存在 200 OK 错误,哪些文件返回 404 NOT found 错误,您可以确定哪个版本正在运行。

If the same files are present across multiple versions, you can inspect deeper into the contents of the files. Either do a byte-by-byte comparison or hash the files and compare the checksums. Hashing and examples of hashing are covered in Chapter 6, Cryptography.

有时,识别版本可能比刚才描述的整个过程简单得多。有时会有一个CHANGELOG.txtreadme.html文件,可以准确地告诉您哪个版本正在运行,而无需做任何工作。

如何防止应用的指纹识别

如前所述,有多种方法可以在技术堆栈的许多不同级别创建指纹应用。你真正应该问自己的第一个问题是,“我需要防止指纹识别吗?”一般来说,试图防止指纹识别是一种混淆。模糊处理有点争议,但我认为每个人都同意模糊处理不是安全性,就像编码不是加密一样。它可能会减慢速度、限制信息或暂时迷惑攻击者,但并不能真正防止任何漏洞被利用。现在,我并不是说模糊处理没有任何好处,但它本身永远不能依赖。模糊只是一层薄薄的隐藏。

Obviously, you don't want to give away too much information about your application, such as debug output or configuration settings, but some information is going to be available no matter what when a service is available on the network. You will have to make a choice about how much time and effort you want to put into hiding information.

有些人甚至输出虚假信息来误导攻击者。就我个人而言,在强化服务器时,我没有列出要做的事情。我建议您做的一件事是删除前面提到的任何额外文件。在部署之前,应删除更改日志文件、默认设置文件、安装文件和文档文件等文件。不要公开提供应用工作不需要的文件。

模糊化是一个值得自己写一章甚至一本书的话题。有专门的模糊处理比赛,奖励最有创意和最奇异的模糊处理形式。有一些工具可以帮助您混淆 JavaScript 代码,但另一方面,也有除臭工具。

使用 goquery 包进行 web 抓取

goquery包不是标准库的一部分,但可在 GitHub 上获得。它的工作原理与 jQuery 类似,jQuery 是一种流行的 JavaScript 框架,用于与 HTMLDOM 交互。如前几节所示,尝试使用字符串匹配和正则表达式进行搜索既繁琐又复杂。goquery包使处理 HTML 内容和搜索特定元素变得更加容易。我之所以推荐这个包,是因为它是根据许多人已经熟悉的非常流行的 jQuery 框架建模的。

您可以通过go get命令获取goquery包:

go get https://github.com/PuerkitoBio/goquery  

文件可在上查阅 https://godoc.org/github.com/PuerkitoBio/goquery

列出页面中的所有超链接

对于goquery包的介绍,我们将看一个常见且简单的任务。我们将在页面中找到所有超链接并打印出来。典型的链接如下所示:

<a href="https://www.devdungeon.com">DevDungeon</a>  

在 HTML 中,a标记代表,而href属性代表超链接引用。可以有一个没有href属性但只有name属性的锚定标记。这些被称为书签或命名锚,用于跳转到同一页面上的某个位置。我们将忽略这些,因为它们仅在同一页面内链接。target属性只是一个可选属性,用于指定打开链接的窗口或选项卡。我们只对本例中的href值感兴趣:

// Load a URL and list all links found
package main

import (
   "fmt"
   "github.com/PuerkitoBio/goquery"
   "log"
   "net/http"
   "os"
)

func main() {
   // Load command line arguments
   if len(os.Args) != 2 {
      fmt.Println("Find all links in a web page")
      fmt.Println("Usage: " + os.Args[0] + " <url>")
      fmt.Println("Example: " + os.Args[0] + 
         " https://www.devdungeon.com")
      os.Exit(1)
   }
   url := os.Args[1]

   // Fetch the URL
   response, err := http.Get(url)
   if err != nil {
      log.Fatal("Error fetching URL. ", err)
   }

   // Extract all links
   doc, err := goquery.NewDocumentFromReader(response.Body)
   if err != nil {
      log.Fatal("Error loading HTTP response body. ", err)
   }

   // Find and print all links
   doc.Find("a").Each(func(i int, s *goquery.Selection) {
      href, exists := s.Attr("href")
      if exists {
         fmt.Println(href)
      }
   })
} 

在网页中查找文档

文件也是关注点。您可能需要刮取网页并查找文档。文字处理器文档、电子表格、幻灯片组、CSV、文本和其他文件可以包含各种用途的有用信息。

下面的示例将搜索 URL,并根据链接中的文件扩展名搜索文档。为了方便起见,在顶部定义了一个全局变量,其中列出了应搜索的所有扩展名。自定义扩展名列表以搜索目标文件类型。考虑扩展应用,以从文件中获取文件扩展名列表,而不是硬编码。在尝试查找敏感信息时,您会查找其他哪些文件扩展名?

以下是此示例的代码实现:

// Load a URL and list all documents 
package main

import (
   "fmt"
   "github.com/PuerkitoBio/goquery"
   "log"
   "net/http"
   "os"
   "strings"
)

var documentExtensions = []string{"doc", "docx", "pdf", "csv", 
   "xls", "xlsx", "zip", "gz", "tar"}

func main() {
   // Load command line arguments
   if len(os.Args) != 2 {
      fmt.Println("Find all links in a web page")
      fmt.Println("Usage: " + os.Args[0] + " <url>")
      fmt.Println("Example: " + os.Args[0] + 
         " https://www.devdungeon.com")
      os.Exit(1)
   }
   url := os.Args[1]

   // Fetch the URL
   response, err := http.Get(url)
   if err != nil {
      log.Fatal("Error fetching URL. ", err)
   }

   // Extract all links
   doc, err := goquery.NewDocumentFromReader(response.Body)
   if err != nil {
      log.Fatal("Error loading HTTP response body. ", err)
   }

   // Find and print all links that contain a document
   doc.Find("a").Each(func(i int, s *goquery.Selection) {
      href, exists := s.Attr("href")
      if exists && linkContainsDocument(href) {
         fmt.Println(href)
      }
   })
} 

func linkContainsDocument(url string) bool {
   // Split URL into pieces
   urlPieces := strings.Split(url, ".")
   if len(urlPieces) < 2 {
      return false
   }

   // Check last item in the split string slice (the extension)
   for _, extension := range documentExtensions {
      if urlPieces[len(urlPieces)-1] == extension {
         return true
      }
   }
   return false
} 

列出页面标题和标题

标题是定义网页层次结构的主要结构元素,<h1>是最高层次,<h6>是最低或最深层次。HTML 页面的<title>标记中定义的标题是浏览器标题栏中显示的内容,它不是呈现页面的一部分。

By listing the title and headings, you can quickly get an idea of what the topic of the page is, assuming that they properly formatted their HTML. There is only supposed to be one <title> and one <h1> tag, but not everyone conforms to the standards.

该程序加载一个网页,然后将标题和所有标题打印到标准输出。尝试在几个 URL 上运行此程序,看看您是否能够通过查看标题快速了解内容:

package main

import (
   "fmt"
   "github.com/PuerkitoBio/goquery"
   "log"
   "net/http"
   "os"
)

func main() {
   // Load command line arguments
   if len(os.Args) != 2 {
      fmt.Println("List all headings (h1-h6) in a web page")
      fmt.Println("Usage: " + os.Args[0] + " <url>")
      fmt.Println("Example: " + os.Args[0] + 
         " https://www.devdungeon.com")
      os.Exit(1)
   }
   url := os.Args[1]

   // Fetch the URL
   response, err := http.Get(url)
   if err != nil {
      log.Fatal("Error fetching URL. ", err)
   }

   doc, err := goquery.NewDocumentFromReader(response.Body)
   if err != nil {
      log.Fatal("Error loading HTTP response body. ", err)
   }

   // Print title before headings
   title := doc.Find("title").Text()
   fmt.Printf("== Title ==\n%s\n", title)

   // Find and list all headings h1-h6
   headingTags := [6]string{"h1", "h2", "h3", "h4", "h5", "h6"}
   for _, headingTag := range headingTags {
      fmt.Printf("== %s ==\n", headingTag)
      doc.Find(headingTag).Each(func(i int, heading *goquery.Selection) {
         fmt.Println(" * " + heading.Text())
      })
   }

} 

在网站上抓取存储最常用单词的网页

这个程序打印出网页上使用的所有单词的列表,以及每个单词在网页中出现的次数。这将搜索所有段落标记。如果你搜索整个网站,它会把所有的 HTML 代码都当作单词来处理,这会把数据弄得乱七八糟,并不能真正帮助你理解网站的内容。它修剪字符串中的空格、逗号、句点、制表符和换行符。它还将所有单词转换为小写,以尝试规范化数据。

对于找到的每个段落,它都会将文本内容分开。每个单词都存储在一个映射中,该映射将字符串映射为整数计数。最后,打印出地图,列出每个单词以及在页面上看到的次数:

package main

import (
   "fmt"
   "github.com/PuerkitoBio/goquery"
   "log"
   "net/http"
   "os"
   "strings"
)

func main() {
   // Load command line arguments
   if len(os.Args) != 2 {
      fmt.Println("List all words by frequency from a web page")
      fmt.Println("Usage: " + os.Args[0] + " <url>")
      fmt.Println("Example: " + os.Args[0] + 
         " https://www.devdungeon.com")
      os.Exit(1)
   }
   url := os.Args[1]

   // Fetch the URL
   response, err := http.Get(url)
   if err != nil {
      log.Fatal("Error fetching URL. ", err)
   }

   doc, err := goquery.NewDocumentFromReader(response.Body)
   if err != nil {
      log.Fatal("Error loading HTTP response body. ", err)
   }

   // Find and list all headings h1-h6
   wordCountMap := make(map[string]int)
   doc.Find("p").Each(func(i int, body *goquery.Selection) {
      fmt.Println(body.Text())
      words := strings.Split(body.Text(), " ")
      for _, word := range words {
         trimmedWord := strings.Trim(word, " \t\n\r,.?!")
         if trimmedWord == "" {
            continue
         }
         wordCountMap[strings.ToLower(trimmedWord)]++

      }
   })

   // Print all words along with the number of times the word was seen
   for word, count := range wordCountMap {
      fmt.Printf("%d | %s\n", count, word)
   }

} 

打印页面中的外部 JavaScript 文件列表

如果您试图对应用进行指纹识别或确定正在加载哪些第三方库,检查页面上包含的 JavaScript 文件的 URL 会有所帮助。该程序将列出网页中引用的外部 JavaScript 文件。外部 JavaScript 文件可能托管在同一个域上,也可能从远程站点加载。检查所有script标签的src属性。

例如,如果 HTML 页面具有以下标记:

<script src="/ajax/libs/jquery/3.2.1/jquery.min.js"></script>  

src属性的 URL 将被打印:

/ajax/libs/jquery/3.2.1/jquery.min.js

注意,src属性中的 URL 可能是完全限定的或相对的 URL。

下面的程序加载一个 URL,然后查找所有的script标记。它将为找到的每个脚本打印src属性。这将只查找外部链接的脚本。要打印内联脚本,请参阅文件底部关于script.Text()的注释。尝试对您经常访问的一些网站运行此功能,查看它们嵌入了多少外部脚本和第三方脚本:

package main

import (
   "fmt"
   "github.com/PuerkitoBio/goquery"
   "log"
   "net/http"
   "os"
)

func main() {
   // Load command line arguments
   if len(os.Args) != 2 {
      fmt.Println("List all JavaScript files in a webpage")
      fmt.Println("Usage: " + os.Args[0] + " <url>")
      fmt.Println("Example: " + os.Args[0] + 
         " https://www.devdungeon.com")
      os.Exit(1)
   }
   url := os.Args[1]

   // Fetch the URL
   response, err := http.Get(url)
   if err != nil {
      log.Fatal("Error fetching URL. ", err)
   }

   doc, err := goquery.NewDocumentFromReader(response.Body)
   if err != nil {
      log.Fatal("Error loading HTTP response body. ", err)
   }

   // Find and list all external scripts in page
   fmt.Println("Scripts found in", url)
   fmt.Println("==========================")
   doc.Find("script").Each(func(i int, script *goquery.Selection) {

      // By looking only at the script src we are limiting
      // the search to only externally loaded JavaScript files.
      // External files might be hosted on the same domain
      // or hosted remotely
      src, exists := script.Attr("src")
      if exists {
         fmt.Println(src)
      }

      // script.Text() will contain the raw script text
      // if the JavaScript code is written directly in the
      // HTML source instead of loaded from a separate file
   })
} 

本例查找由src属性引用的外部脚本,但有些脚本是在开始和结束script标记之间直接用 HTML 编写的。这些类型的内联脚本不会有src属性引用。使用goquery对象上的.Text()函数获取内联脚本文本。参考本例底部,其中提到了script.Text()

该程序不打印内联脚本,而只关注外部加载的脚本的原因是,这是因为引入了很多漏洞。加载远程 JavaScript 是有风险的,应该仅使用受信任的源来完成。即使如此,我们也不能 100%保证远程内容提供商不会受到危害,不会提供恶意代码。考虑一下像雅虎这样的大公司吧!世卫组织已公开承认其系统在过去遭到破坏。雅虎!还有一个广告网络,承载着一个内容交付网络CDN),该网络向大型网站网络提供 JavaScript 文件。这将是攻击者的主要目标。当在敏感的客户门户中包含远程 JavaScript 文件时,考虑这些风险。

深度优先爬行

深度优先爬网是指将同一域上的链接优先于指向其他域的链接。在这个程序中,外部链接被完全忽略,只遵循同一域上的路径或相对链接。

在本例中,唯一路径存储在切片中,并在最后一起打印。爬网过程中遇到的任何错误都将被忽略。由于链接格式错误,经常会遇到错误,我们不希望整个程序在出现这样的错误时退出。

使用url.Parse()函数,而不是尝试使用字符串函数手动解析 URL。它将主机与路径分离。

爬网时,将忽略任何查询字符串和片段以减少重复。查询字符串在 URL 中用问号指定,片段(也称为书签)用磅或哈希符号指定。此程序是单线程的,不使用 goroutines:

// Crawl a website, depth-first, listing all unique paths found
package main

import (
   "fmt"
   "github.com/PuerkitoBio/goquery"
   "log"
   "net/http"
   "net/url"
   "os"
   "time"
)

var (
   foundPaths  []string
   startingUrl *url.URL
   timeout     = time.Duration(8 * time.Second)
)

func crawlUrl(path string) {
   // Create a temporary URL object for this request
   var targetUrl url.URL
   targetUrl.Scheme = startingUrl.Scheme
   targetUrl.Host = startingUrl.Host
   targetUrl.Path = path

   // Fetch the URL with a timeout and parse to goquery doc
   httpClient := http.Client{Timeout: timeout}
   response, err := httpClient.Get(targetUrl.String())
   if err != nil {
      return
   }
   doc, err := goquery.NewDocumentFromReader(response.Body)
   if err != nil {
      return
   }

   // Find all links and crawl if new path on same host
   doc.Find("a").Each(func(i int, s *goquery.Selection) {
      href, exists := s.Attr("href")
      if !exists {
         return
      }

      parsedUrl, err := url.Parse(href)
      if err != nil { // Err parsing URL. Ignore
         return
      }

      if urlIsInScope(parsedUrl) {
         foundPaths = append(foundPaths, parsedUrl.Path)
         log.Println("Found new path to crawl: " +
            parsedUrl.String())
         crawlUrl(parsedUrl.Path)
      }
   })
}

// Determine if path has already been found
// and if it points to the same host
func urlIsInScope(tempUrl *url.URL) bool {
   // Relative url, same host
   if tempUrl.Host != "" && tempUrl.Host != startingUrl.Host {
      return false // Link points to different host
   }

   if tempUrl.Path == "" {
      return false
   }

   // Already found?
   for _, existingPath := range foundPaths {
      if existingPath == tempUrl.Path {
         return false // Match
      }
   }
   return true // No match found
}

func main() {
   // Load command line arguments
   if len(os.Args) != 2 {
      fmt.Println("Crawl a website, depth-first")
      fmt.Println("Usage: " + os.Args[0] + " <startingUrl>")
      fmt.Println("Example: " + os.Args[0] + 
         " https://www.devdungeon.com")
      os.Exit(1)
   }
   foundPaths = make([]string, 0)

   // Parse starting URL
   startingUrl, err := url.Parse(os.Args[1])
   if err != nil {
      log.Fatal("Error parsing starting URL. ", err)
   }
   log.Println("Crawling: " + startingUrl.String())

   crawlUrl(startingUrl.Path)

   for _, path := range foundPaths {
      fmt.Println(path)
   }
   log.Printf("Total unique paths crawled: %d\n", len(foundPaths))
} 

广度优先爬行

广度优先的爬行是指优先寻找新的域并尽可能地扩展,而不是以深度优先的方式继续通过单个域。

根据本章提供的信息,编写广度优先爬虫将留给读者作为练习。它与上一节中的深度优先爬虫没有太大的不同,只是它应该对指向以前未见过的域的 URL 进行优先级排序。

有几个注意事项需要记住。如果您不小心,并且没有设置最大限制,您可能最终会抓取数 PB 的数据!你可以选择忽略子域,或者你可以进入一个有无限个子域的站点,你将永远不会离开。

如何防止网页刮花

即使不是不可能,也很难完全防止刮纸。如果您从 web 服务器提供信息,将有一种以编程方式提取数据的方法。你只能设置障碍。这相当于混淆,你可以说这不值得努力。

JavaScript 使其变得更加困难,但并非不可能,因为 Selenium 可以驱动真正的 web 浏览器,并且可以使用 PhantomJS 等框架来执行 JavaScript。

需要身份验证有助于限制执行的刮取量。速率限制也可以提供一些缓解。速率限制可以使用诸如 iptables 之类的工具完成,也可以基于 IP 地址或用户会话在应用级别完成。

检查客户机提供的用户代理是一个肤浅的措施,但可以帮助一点。放弃用户代理附带的请求,这些请求包括关键字,如curlwgetgopythonrubyperl。阻止或忽略这些请求可以防止简单的机器人抓取您的站点,但客户端可以伪造或忽略其用户代理,以便轻松绕过。

如果您想更进一步,可以将 HTML ID 和类名设置为动态的,这样就不能使用它们来查找特定信息。经常改变你的 HTML 结构和命名,玩猫捉老鼠游戏,让它比刮板机的价值更大。这不是一个真正的解决方案,我不推荐它,但值得一提,因为它在刮刀的眼中是令人讨厌的。

在显示数据之前,可以使用 JavaScript 检查有关客户端的信息,例如屏幕大小。如果屏幕大小是 1 x 1 或 0 x 0,或者是一些奇怪的东西,您可以假设它是一个机器人并拒绝渲染内容。

蜜罐形式是检测机器人行为的另一种方法。使用 CSS 或hidden属性隐藏表单字段,并检查这些字段中是否提供了值。如果数据位于这些字段中,则假设机器人正在填写所有字段并忽略请求。

另一种选择是使用图像来存储信息,而不是文本。例如,如果只输出饼图的图像,则与将数据作为 JSON 对象输出并使用 JavaScript 呈现饼图相比,对某人来说,刮取数据要困难得多。scraper 可以直接获取 JSON 数据。文本也可以放置在图像中,以防止文本被刮伤,并防止关键字文本搜索,但光学字符识别OCR)可以通过一些额外的努力解决这一问题。

根据应用情况,前面的一些技术可能很有用。

总结

阅读本章后,您现在应该了解 web 抓取的基本原理,例如执行 HTTPGET请求,并使用字符串匹配或正则表达式搜索字符串以查找 HTML 注释、电子邮件和其他关键字。您还应该了解如何提取 HTTP 头并设置自定义头以设置 Cookie 和自定义用户代理字符串。此外,您应该了解指纹识别的基本概念,并了解如何根据提供的源代码收集有关 web 应用的信息。

读完本章之后,您还应该了解使用goquery包以 jQuery 样式在 DOM 中查找 HTML 元素的基础知识。在网页中查找链接、查找文档、列出标题和标题、查找 JavaScript 文件,以及查找广度优先和深度优先爬网之间的差异,您应该会感觉很舒服。

关于删除公共网站的注意事项要尊重他人。不要通过发送大批量或让爬虫不受限制地运行,从而给网站带来不合理的流量。为您编写的程序设置合理的速率限制和最大页数限制,以避免远程服务器负担过重。如果您正在抓取数据,请始终检查 API 是否可用。API 的效率更高,并且旨在以编程方式使用。

你能想出其他方法来应用本章中所研究的工具吗?您能想到可以添加到提供的示例中的其他功能吗?

在下一章中,我们将介绍主机发现和枚举的方法。我们将介绍 TCP 套接字、代理、端口扫描、横幅抓取和模糊化等内容。