常见语言及工具发包速度对比测试

背景

在安全测试中,经常会遇到需要进行暴力破解账号密码的场景,用于测试系统是否存在弱密码,或者进行用户名枚举等。对于此种需求,我们主要需要的是操作简单、功能强大、测试速度快、测试结果准确,目前已经有许多工具可以支持这种功能,最常用的如BurpSuite、Hydra等。但在实际的测试过程中经常也会遇到各种特殊的场景,现有的工具不能很好提供很好的支持,此时多数使用自己编写的脚本。目前主流的编程语言均支持发送HTTP的功能,但发包速度各不相同,而对于暴力破解而言,发包速度无疑是重中之重,另外各种语言常见的WEB框架响应时间也不相同。为了详细了解不同语言、框架、工具的发包速度和响应速度,故进行如下场景测试。

概述

本次测试分为两大部分,一个是发送端,主要工具如下,其中部分语言或工具可能并不适合,仅用于对比:

工具 版本
Python3-requests Python-3.7.3+requests-2.21.0
Python3-aiohttp Python-3.7.3+aiohttp-3.5.4
C# .NET Core-3.0+C#-8.0
Java java-1.8.0_211
Golang go-1.13.1
Nodejs-request node-v10.16.0+request-2.88.0
Php php-7.3.4
Ruby ruby-2.3.3p222
Shell-curl curl-7.64.0
BurpSuite BurpSuite-2.0.11beta
Hydra Hydra-v8.8

另一部分是常见的WEB后端框架:

工具 版本
Python3-flask Python-3.7.3+flask-1.1.1
Python3-sanic Python-3.7.3+sanic-19.6.3
Ruby-ruby on rails ruby-2.3.3p222+Rails-5.1.3
Java-SpringBoot java-1.8.0_211+SpringBoot-2.1.9.RELEASE
Golang go-1.13.1
Php php-7.3.4
Nodejs-express node-v10.16.0+express-4.17.1
C#-ASP.NET .NET Core-3.0+C#-8.0

在一次完整的HTTP请求中,主要可分为以下几个部分:

  1. 准备HTTP请求
  2. 发送HTTP请求
  3. 解析HTTP请求,处理并准备HTTP响应
  4. 返回HTTP响应
  5. 解析并处理HTTP响应

在以上5个步骤中,第1和5步主要由发送端的速度决定,第2和4步主要由网络情况决定,第3步主要由WEB服务器及后端服务决定。

准备

测试方法:服务端模拟账号密码验证,密码为字典的最后一项,密码字典大约为10k。发送端模拟爆破并计时,多次测试后统计爆破平均用时。

以下是各工具发送端的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Python3-requests
# pip install requests
import requests
import datetime
start = datetime.datetime.now()
url = 'http://127.0.0.1:5000'
with open('pass10k.txt', 'r', encoding="utf-8") as f:
passwords = [i.strip() for i in f.readlines()]
data = {
"username": "admin",
"password": ""
}
session = requests.session()
for password in passwords:
data['password'] = password
res = session.post(url, data=data)
if res.text == '登录成功':
end = datetime.datetime.now()
print(end-start)
break
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
# python3-requests多线程
# pip install requests
import requests
import datetime
from concurrent.futures import ThreadPoolExecutor
start = datetime.datetime.now()
url = 'http://127.0.0.1:5000'
with open('pass10k.txt', 'r', encoding="utf-8") as f:
passwords = [i.strip() for i in f.readlines()]
class State(object):
def __init__(self):
self.stop = False # 找到密码后为True
self.passwd = ''
def test(p):
if not state.stop:
data = {
"username": "admin",
"password": p
}
res = requests.post(url, data=data)
if res.text == '登录成功':
state.passwd = p
state.stop = True
state = State()
with ThreadPoolExecutor(max_workers=32) as pool:
results = pool.map(test, passwords)
end = datetime.datetime.now()
print(end-start)
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
# Python3-aiohttp,异步
# pip install aiohttp
import datetime
import asyncio
from aiohttp import ClientSession
start = datetime.datetime.now()
url = 'http://127.0.0.1:5000'
with open('pass10k.txt', 'r', encoding="utf-8") as f:
passwords = [i.strip() for i in f.readlines()]
class State(object):
def __init__(self):
self.stop = False # 找到密码后为True
self.passwd = ''
async def test(p):
if not state.stop:
data = {
"username": "admin",
"password": p
}
async with ClientSession() as session:
async with session.post(url, data=data) as response:
response = await response.read()
if response.decode() == '登录成功':
state.passwd = p
state.stop = True
async def get(p, sem):
async with sem:
await test(p)
state = State()
loop = asyncio.get_event_loop()
sem = asyncio.Semaphore(16)
tasks = [get(p, sem) for p in passwords]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
end = datetime.datetime.now()
print(end-start)
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
// C#-HttpWebRequest
using System;
using System.IO;
using System.Net;
using System.Text;
namespace hello
{
class Program
{
static void Main(string[] args)
{
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
watch.Start();
string[] passwords = System.IO.File.ReadAllLines("pass10k.txt");
foreach (string password in passwords)
{
HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://127.0.0.1:5000");
req.Method = "POST";
req.ContentType = "application/x-www-form-urlencoded";
byte[] data = Encoding.UTF8.GetBytes("username=admin&password="+password);
req.ContentLength = data.Length;
using (Stream reqStream = req.GetRequestStream())
{
reqStream.Write(data, 0, data.Length);
reqStream.Close();
}
HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
Stream stream = resp.GetResponseStream();
using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
{
string response = reader.ReadToEnd();
resp.Close();
if (response == "登录成功")
{
watch.Stop();
Console.WriteLine("耗时:" + (watch.ElapsedMilliseconds)+"ms");
Console.WriteLine(password);
break;
}
}
}
}
}
}
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
61
62
63
64
65
66
67
// Java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;
public class Test {
public static void main(String[] args) throws Exception {
String url = "http://127.0.0.1:5000";
String data = "username=admin&password=";
BufferedReader in = new BufferedReader(new FileReader("pass10k.txt"));
String str;
long startTime=System.currentTimeMillis();
while ((str = in.readLine()) != null) {
String res = sendPost(url,data+str);
if (res.equals("登录成功")) {
System.out.println(res+":"+str);
break;
}
}
in.close();
long endTime=System.currentTimeMillis();
System.out.println("程序运行时间: "+(endTime-startTime)/1000+"s");
}
public static String sendPost(String url, String param) {
PrintWriter out = null;
BufferedReader in = null;
String result = "";
try {
URL realUrl = new URL(url);
URLConnection conn = realUrl.openConnection();
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
conn.setDoOutput(true);
conn.setDoInput(true);
out = new PrintWriter(conn.getOutputStream());
out.print(param);
out.flush();
in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("发送 POST 请求出现异常!"+e);
e.printStackTrace();
}
finally{
try{
if(out!=null){
out.close();
}
if(in!=null){
in.close();
}
}
catch(IOException ex){
ex.printStackTrace();
}
}
return result;
}
}
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
// Golang-net/http
package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
)
func main() {
t1 := time.Now()
inputFile, _ := os.Open("pass10k.txt")
defer inputFile.Close()
inputReader := bufio.NewReader(inputFile)
for {
inputString, readerError := inputReader.ReadString('\n')
resp, _ := http.Post("http://127.0.0.1:5000/",
"application/x-www-form-urlencoded",
strings.NewReader("username=admin&password="+strings.Replace(inputString, "\n", "", -1)))
body, _ := ioutil.ReadAll(resp.Body)
if string(body)=="登录成功"{
elapsed := time.Since(t1)
fmt.Println(string(body)+":"+inputString)
fmt.Println(elapsed)
break
}
if readerError == io.EOF {
return
}
resp.Body.Close()
}
}
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
// Nodejs-request
// npm install --save request bluebird
const request = require('request');
const fs = require("fs");
const Promise = require('bluebird')
var start = Date.now();
function getData(i){
return new Promise(function(resolve, rej){
count += 1;
request.post({url:'http://127.0.0.1:5000', form:{"username":'admin',"password":i}}, function(error, response, body) {
if (body == "登录成功"){
console.log(i);
console.log((Date.now()-start)+"ms");
rej();
}
else{
resolve();
}
})
})
}
fs.readFile('pass10k.txt', function (err, data) {
var pass = data.toString().split("\n");
Promise.map(pass, i=>getData(i), {concurrency: 16});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
ini_set("max_execution_time", 1800);
// 可能还需要修改apache超时设置,参考https://codeday.me/bug/20190428/998884.html
$file_handle = fopen('pass10k.txt', "r");
$t1 = microtime(true);
while (!feof($file_handle)) {
$line = fgets($file_handle);
$data = array('username' => 'admin','password'=>$line);
$data = http_build_query($data);
$opts = array('http' => array('method' => 'POST', 'header' => 'Content-type: application/x-www-form-urlencoded', 'content' => $data));
$context = stream_context_create($opts);
$html = file_get_contents('http://127.0.0.1:5000', false, $context);
if ($html==="登录成功") {
echo $line;
$t2 = microtime(true);
echo '<br />耗时'.round($t2-$t1,3).'秒';
break;
}
}
fclose($file_handle);
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Ruby
require 'net/http'
passwd = IO.readlines("pass10k.txt")
t1=Time.now
passwd.each do |i|
params = {"username"=>"admin","password"=>i}
uri = URI.parse("http://127.0.0.1:5000/")
res = Net::HTTP.post_form(uri, params)
text = res.body.force_encoding('utf-8')
if text=="登录成功"
puts i
t2=Time.now
puts t2-t1
break
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
start=$(date "+%s")
while read line
do
res=`curl http://127.0.0.1:5000 -d "username=admin&password=${line}" -s`
if [ "$res" == "登录成功" ]
then
echo $res": "$line
break
fi
done < pass10k.txt
now=$(date "+%s")
time=$((now-start))
echo "${time}s"

BurpSuite: 在BurpSuite中抓包,然后在Intruder模块中设置payload即可.

1
2
# Kali Linux - Hydra
hydra 127.0.0.1 -s 5000 -l admin -P pass10k.txt -f -V http-post-form "/:username=^USER^&password=^PASS^:失败" -t 16 -vV -e ns

以下是服务端的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Python3-flask
# pip install flask
from flask import Flask, request
app = Flask(__name__)
@app.route('/', methods=["POST"])
def post():
username = request.form['username']
password = request.form['password']
if username=='admin' and password=='Test@123':
return "登录成功"
else:
return "登录失败"
if __name__ == "__main__":
app.run()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Python3-sanic
# pip install sanic
# sanic是python的异步web框架,仅支持python3.6+,用于对比nodejs
from sanic import Sanic
from sanic.response import text
app = Sanic(__name__)
@app.route("/", methods=['POST'])
async def post(request):
username = request.form.get('username',"")
password = request.form.get('password',"")
if username=='admin' and password=='Test@123':
return text("登录成功")
else:
return text("登录失败")
app.run(host="127.0.0.1", port=5000, debug=False)# 必须为false,否则慢很多
1
2
3
4
5
6
7
8
9
10
11
12
13
# Ruby-ruby on rails
# routes.rb中路由设置为: post '/' => 'login#login'
class LoginController < ApplicationController
def login
username = request.POST["username"]
password = request.POST["password"]
if username=="admin" && password=="Test@123"
response.stream.write "登录成功"
else
response.stream.write "登录失败"
end
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Java-SpringBoot
package com.example.demo.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
@PostMapping(value = "login")
public String login(@RequestParam String username, @RequestParam String password) {
if (username.equals("admin") && password.equals("Test@123")){
return "登录成功";
}
return "登录失败";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Golang
package main
import (
"fmt"
"net/http"
)
func login(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
if len(r.Form["username"])>0 && len(r.Form["password"])>0 && r.Form["username"][0]=="admin" && r.Form["password"][0]=="Test@123" {
fmt.Fprintf(w, "登录成功")
} else {
fmt.Fprintf(w, "登录失败")
}
}
func main() {
http.HandleFunc("/", login)
http.ListenAndServe(":9001", nil)
}
1
2
3
4
5
6
7
8
9
<?php
$username = $_POST['username'];
$password = $_POST['password'];
if ($username=='admin' && $password=='Test@123'){
echo "登录成功";
}else{
echo "登录失败";
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Nodejs-express
// npm install express
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var urlencodedParser = bodyParser.urlencoded({ extended: false })
app.post('/', urlencodedParser, function (req, res) {
if (req.body.username=="admin" && req.body.password=="Test@123"){
res.send('登录成功');
}else{
res.send('登录失败');
}
})
app.listen(8081, () => {})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ASP.NET Core WEB - C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace web.Controllers
{
[Route("/")]
[ApiController]
public class LoginController : ControllerBase
{
[HttpPost]
public string Login()
{
string username = Request.Form["username"];
string password = Request.Form["password"];
if (username == "admin" && password == "Test@123") return "登录成功";
else return "登录失败";
}
}
}

结果

发送速度对比:

发送端 服务端 线程数 密码数 网络延时 耗时(秒) 备注
Python3-requests Python3-flask 1 10k 38 稳定
Python3-aiohttp Python3-flask 1 10k 15 无序,全部跑完约15秒
C# Python3-flask 1 10k 27 误差2秒
Java Python3-flask 1 10k 18 稳定
Golang Python3-flask 1 10k 18 稳定
Nodejs-request Python3-flask 1 10k 15 无序,全部跑完约15秒
Php Python3-flask 1 10k 53 不稳定,误差5秒
Ruby Python3-flask 1 10k 120 稳定
Shell-curl Python3-flask 1 10k 412 太慢
BurpSuite Python3-flask 1 10k 420 太慢
Hydra Python3-flask 1 10k 1h56m 单线程慢到不能用

静态语言(C#/Java/Golang)普遍快于动态语言(Python/Ruby/Php),但动态语言如果采用异步框架(Python3-aiohttp/Nodejs-request)的话,则可以达到与静态语言的相当水品。而反观现成的工具(curl/BurpSuite/Hydra)等,虽然它们功能强大使用简单方便,但在单线程的情况下速度慢到几乎不能用,完全不能与手动编码的工具相比。

不过以上只是测试在本地的情况,一般情况下网络延时都会远远大于其他部分的耗时,故下面模拟延迟200ms(服务端等待200ms后返回)的网络情况再次测试,不过由于加上延时后速度慢很多,所以将密码数量下降至1K。

发送端 服务端 线程/并发数 密码数 网络延时 耗时(秒)
Python3-requests Python3-flask 1 1k 200ms 206
C# Python3-flask 1 1k 200ms 204
Java Python3-flask 1 1k 200ms 204
Golang Python3-flask 1 1k 200ms 203
Php Python3-flask 1 1k 200ms 207
Ruby Python3-flask 1 1k 200ms 208
Shell-curl Python3-flask 1 1k 200ms 245
BurpSuite Python3-flask 1 1k 200ms 205
Hydra Python3-flask 1 1k 200ms 961

可以看到使用编程语言编写的工具,无论是静态还是动态差距都不大,相比于没有网络延迟时,速度大约下降了50-100倍;而现有的工具,下降则并不多,curl和BurpSuite大约只下降了5倍,而Hydra大约只慢了1/3。故可以看出,手动编写的工具瓶颈在于网络及服务端,自身速度很快;而现有工具则是自身速度不够快,网络及服务端的影响则较小。

要想真正达到高速爆破,还是得使用多线程和异步框架:

发送端 服务端 线程/并发数 密码数 网络延时 耗时(秒)
Python3-requests Python3-flask 16 1k 200ms 13
Python3-aiohttp Python3-flask 16 1k 200ms 13
Nodejs-request Python3-flask 16 1k 200ms 14
BurpSuite Python3-flask 16 1k 200ms 59
Hydra Python3-flask 16 1k 200ms 57

可以看到在16线程时,相比于单线程,手动编写的工具速度大约提升了15倍,BurpSuite大约提升了3倍多,Hydra提升了17倍,异步框架与多线程相当。

多线程的速度明显加强,那么到底多少线程合适又成了另外一个问题,继续测试:

发送端 服务端 线程/并发数 密码数 网络延时 耗时(秒)
Python3-requests Python3-flask 1 1k 200ms 206
Python3-requests Python3-flask 2 1k 200ms 103
Python3-requests Python3-flask 4 1k 200ms 52
Python3-requests Python3-flask 8 1k 200ms 26
Python3-requests Python3-flask 16 1k 200ms 13
Python3-requests Python3-flask 32 1k 200ms 7
Python3-requests Python3-flask 64 1k 200ms 3.5
Python3-requests Python3-flask 128 1k 200ms 2.6
Python3-requests Python3-flask 256 1k 200ms 2.6
Python3-requests Python3-flask 512 1k 200ms 2.7
Python3-requests Python3-flask 1024 1k 200ms 2.7

在线程数较少时,线程数量越多,速度越快,几乎与线程数成正比,但是在到达一定数量后速度就不再变化了,这里有两种可能,一种是发送速度达到极限,另一种可能是服务端响应速度达到极限,于是开始考虑更换更快的服务端框架。

目前主流的后端框架响应时间对比如下:

发送端 服务端 线程/并发数 密码数 网络延时 耗时(秒)
Python3-aiohttp Python3-flask 64 100k 164
Python3-aiohttp Python3-sanic 64 100k 108
Python3-aiohttp Ruby-ruby on rails 64 100k 340
Python3-aiohttp Java-SpringBoot 64 100k 125
Python3-aiohttp Golang 64 100k 95
Python3-aiohttp Php 64 100k 193
Python3-aiohttp Nodejs-express 64 100k 115
Python3-aiohttp C#-ASP.NET 64 100k 147

由于没有采用专门的部署方式,只是使用各工具自带的调试模式,所以部分框架测试结果存在较大差距。不过从上面的结果也可以看出静态语言和异步框架是明显强于动态语言的。

继续测试多线程的情况,现在服务端改为Golang,网络延迟继续调成200ms,结果如下:

发送端 后端 线程/并发数 密码数 网络延时 耗时(秒)
Python3-requests Golang 64 10k 200ms 40
Python3-requests Golang 128 10k 200ms 29
Python3-requests Golang 256 10k 200ms 23
Python3-requests Golang 512 10k 200ms 19
Python3-requests Golang 1024 10k 200ms 17
Python3-requests Golang 2048 10k 200ms 17

可以看到线程数继续上升后,耗时依然在减少,说明线程数提升还是可以提升发送速度的。但是在实际的环境中,存在瓶颈的多半还是在网络速度和服务端响应速度上,发送速度到达一定程度后对总体而言不会再有提升。另外,若果想要提升总体速度,使用现有的工具任然是不够的,还是使用编程语言编写的工具速度会快很多。

总结

对于发送端而言,静态语言一般快于动态语言、多线程快于单线程、异步快于同步、简单工具快于复杂工具。

在网络情况较差或服务端响应速度较慢的情况下,使用现有的工具(BurpSuite、Hydra等)并开启多线程是足够的;但在网络情况良好且服务端响应较快时,现有工具的速度则不够用了,应该使用支持多线程或协程、编码简单并且自己较为熟悉的语言,python/java/go/nodejs/c#等都是不错的选择。

对于后端框架而言,静态语言同样快于动态语言、异步快于同步,不过这种响应速度在实际场景中意义不大,因为大多数场景下其他操作的时间会远远大于这里的响应时间,且实际部署方式也会有所不同,这里的测试结果只能用于参考。