简单的四位数字

访问靶场,页面提示用户名是admin,密码是四位纯数字。

image-20240131171255669

接着输入用户名 admin ,密码随便输入,使用 burp 抓包即可,右击空白处,选择 Send to intruder

image-20240131171441577

接着来到 Intruder 模块,由于这里我们知道用户名为 admin,但是不知道密码,故而攻击模式选择 Sniper 模块即可。

然后选中密码的值,点击 Add 设置为变量。

image-20240131171553799

将密码的值设置为变量之后,我们点击 Payloads 设置密码字典。由于密码是某个范围的纯数字,因此 Payload Type 选择 Numbers

image-20240131172017791

最后点击 Start attack 即可。

image-20240131172035743

最后等待暴破即可,由于登陆成功和登陆失败返回的内容长度是不一样的,故而可以通过长度值排序,定位到登陆成功的那条。

image-20240131172245411

Top100

访问靶场,提示用户名是 admin 密码是弱口令,我们这里可以加载弱口令字典进行暴破。具体字典可以通过 github 自行收集,我这里使用我特意制作的字典。

image-20240131172332280

同上一关一样,先进行抓包,然后将其发送到 Intruder 模块。然后依旧将密码处设置为变量。

image-20240131172630930

点击 Payloads 设置密码字典,可以是一个列表,也可以是一个文件,如果是从字典文件读取密码到 burp 中,将 Payload Type 设置为 Simple list ,如果是在爆破的时候直接从字典中读取密码,选择 Runtime file

image-20240131172948999

最后点击 Start attack注意:这里的字典路径,建议不要使用带有中文的路径,不然可能会出错。

image-20240131173057671

依旧是根据返回长度不同来判断登陆是否成功,当然有些网站登录成功可能会进行 301 跳转,则那时可以通过响应状态码判断,最终得到密码 qewr1234

Top100 Json

来到靶场,通过查看页面源代码,发现是以 JSON 格式传入的用户名和密码。

image-20240131173405327

老套路,burp 抓包 然后发送到 Intrude 模块,只是传输数据的格式不同,实际上暴破方法还是一样的。

image-20240131173504002

设置密码为变量

image-20240131173609985

设置字典

image-20240131173704182

暴破结果

image-20240131173730523

得到密码为 zxcvbnm,同时服务器返回的数据也是 JSON 格式。

不失效的验证码

访问靶场发现登陆页面存在验证码,用户名 admin,密码为弱口令。

image-20240131174530517

经过测试,发现登陆时确实需要输入验证码,并且需要与显示验证码匹配。我们登陆时抓取数据包,右键将其发送到 Repeater 模块,叫做重发器,我们可以在这里进行重复发送数据包。

image-20240131174918547

经过多次发送测试发现,只要在 Proxy 模块中的数据包不放,则这里的验证码一直是有效的。

image-20240131174856392

故而该验证码就是不失效的验证码,可以重复利用。但是如果把 Proxy 模块中的数据包放掉,则会是一个重新的请求,重新获取验证码,原验证码失效,我们可以进行测试,将 Proxy 数据包放掉。

image-20240131175149904

再次在重发器中发送数据包,发现提示验证码错误。那么我们就知道,只要不把抓的数据包放掉,则该验证码可以重复使用,故而重新抓包,将其发送到 Intruder 模块。

image-20240131175317365

设置变量

image-20240131175406637

设置字典

image-20240131175443604

暴破结果

image-20240131175525282

得到密码为 qwe123..

前端加密

来到靶场,发现用户名为 admin 密码提示为 Burp 自带的字典。

image-20240131175719811

使用 Burp 进行抓包,发现输入的密码以加密的形式传给了服务器,先将其发送到 Intruder 模块。

image-20240131175643207

接着来到网页源码中查看,发现在提交的时候会触发 Javascript 的 md5_enc() 函数,该函数的作用则是将密码进行 md5 加密,然后传给服务器。

image-20240131175938363

因此,服务器接受到的密码是以 MD5 形式进行传输的。故而我们在暴破的时候,也需要将我们使用的密码进行 MD5 加密,Burp 工具是提供了这一功能的。

设置变量

image-20240131180206917

设置字典

image-20240131180354566

这里需要将字典的值以 MD5 加密之后 发给服务器。

暴破结果

image-20240131180449301

得到密码的MD5值为:52763ce1866767ba49826556ee37571c,解密之后得到 Fucker

HTTP Basic 认证

访问靶场出现让我们输入用户名和密码:

image-20240131190614396

当我们点击 取消 则返回 401 状态码。并提示我们未认证。

image-20240131190652367

再次刷新页面,则需要重新输入用户名和密码。这里使用的是 HTTP Basic 认证,中文名称为 简单认证。它通常通过中间件的设置,让用户访问某个目录下的文件需要输入用户名和密码。

tomcat 的管理后台使用的就是该认证。

这里说下该认证的原理:

image-202401310101

接着我们输入用户名 admin 密码随便输入,使用 burp 进行抓包,框选 Basic 后面的 base64 编码,然后右键选择 Convert selection -> Base64 -> Base64-decode 对其进行 base64 编码。

image-20240131192041553

发现这里确实如上所说,用户名和密码以冒号进行分割, 并进行 base64 编码后发给了服务器,右键将其发送到 Intruder 模块。

image-20240131192211521

用户名还为 admin,密码是未知的,故而我们这里将其全选,然后设置为变量。攻击类型为 Sniper

image-20240131192346581

接着设置字典,刚刚提到,Basic 认证是以 用户名:密码 的形式进行 base64 编码之后发送给服务器的,故而我们这里也需要将字典的内容进行编码。

Payload processing 中点击 add ,设置前置为 admin:,然后点击(Encode) 选择 Base64-encode,设置这里的作用是,当 burp 工具从字典中读取每个密码的时候,都会在其前面加上 admin:,例如 burp 读取的字段内容为 qwer1234,经过添加前缀之后就为 admin:qwer1234 ,最后经过 base64 编码就为 admin:qwer1234 的 base64 编码。

由于 base64 编码的内容可能会存在诸如 = 和 + 的内容,burp 默认会对其进行 URL 编码,这里在 Payloads 选项卡底部取消勾选不编码即可。

image-20240131193121447

image-20240131192600171

最后点击 start attack 即可。

前面提到过,basic 在认证成功的时候会返回状态码 200,故而这里也可以通过状态码进行排序。

image-20240131193235701

由于用户名和密码是经过 base64 编码的,故而我们这里右键解码即可。最终得到密码 admin123

image-20240131193409778

返回特征匹配

访问靶场,得到如下提示:

image-20240131194157561

发现登陆失败返回如下内容:

image-20240131193724027

这一关则考的是,登陆成功和登陆失败,返回的内容长度以及状态码是相同的,前面我们都是通过状态码和内容长度来判断登陆是否成功,那么这一关,我们该如何判断呢?

其实,尽管成功和失败返回内容的长度一致,但是成功和失败返回的内容肯定是截然不同的,因此我们可以提取登陆失败返回的值,然后在诸多数据包中进行标记,没有该标记的则就是成功的包。

那么这里,登陆失败返回的特征则是存在内容 x1ongsec{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx} ,这里可能会问,为什么不匹配登陆失败这个关键字呢?众所周知,burp 是国外所开发的工具,故而对中文适配性不是很好。因此尽量匹配英文特征。

按照往常一样,先使用 burp 抓取登陆时的数据包,然后将其发送到 Inteuder

we

设置字典

image-20240131203318993

设置返回特征匹配

来到 Intruder 模块的 Settings 选项中,然后找到 Grep Match 先点击 clear 将其清空。

image-20240131203515425

接着将登陆失败的特征 x1ongsec{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx} 设置到其中。

image-20240131203648609

最后点击 Start attack 开始即可。

经过排序,发现当 密码为 admin123 时,则没有匹配到内容 x1ongsec{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}。查看其响应包,发现为登陆成功的数据包。

image-20240131204105495

故而密码为 admin123

数据库泄露之撞库

这里提供了字典的下载,点击将字典下载即可。

image-20240131204252223

简单的说一下撞库的概念,意思是拿到某公司网站A的数据库,使用其密码登录网站B。称之为撞库。

image-20240131204408205

第一列为用户名,第二列为密码。这里可以使用 Linux 系统的 awk 命令获取第一行和第二行的内容,并将其输出到相应文件中。

1
2
root@ls-rK8rbuXz:~# awk '{print $1}' db.txt  >> username.txt
root@ls-rK8rbuXz:~# awk '{print $2}' db.txt >> password.txt

接着使用 bp 抓取 登陆时的数据包,然后将其发送到 intruder 模块。

image-20240131205146129

这里与以往不同的是,我们并不知道用户名以及密码,都需要从字典文件中读取,故而 attack type 需要选择 Pitchfork,让用户名参数和密码参数都拥有自己独立的字典。

如果不太了解 burpsuite 工具的 Intrude 相应的 attack type 请移步百度。

image-20240131205715381

为参数1用户名处设置字典:

image-20240131205815497

为参数2密码处设置字典:

image-20240131205850557

之后开始攻击即可。

image-20240131210153214

得到用户名 liujing 密码 qweasdzxc

时间戳 token

image-20240131210248185

使用 burp 抓取登陆时的数据包,发现存在 token 。token 是一种由服务器生成的一段校验值,然后返回给用户,用户每次请求时需要带着服务器生成的 token 去请求。不然服务器会认为这不是用户发起的请求。

image-20240131210341891

综上所述,就增加了我们暴破的难度,由于是在用户请求时就会在 POST 请求体里面提交 token,故而 token 肯定会在前端显示,我们查看页面源代码。

image-20240131211241622

发现这里存在隐藏的输入框 token ,当用户点击登陆的时候,会触发 JavaScript 的 md5_encrypt 方法,会将当前时间戳加密成 md5 之后作为 token 的值,提交给服务器。

故而我们这里知道,token 值的生成是有迹可循的,也就是说,提交的时间戳进行 md5 加密之后 就是 token。

时间戳是从 1970 年 1 月 1 日(UTC/GMT的午夜)开始所经过的秒数

但是由于 burp 自身无法生成当前时间戳的字典,只能是当前日期。

image-20240131211932729

所以需要编写 Python 脚本:

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
import requests
import time
from hashlib import md5


url = 'http://172.16.19.1:30028/'
# 获取子字典内容
passwords = open('passwords.txt',mode='r').readlines()
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
}
# 遍历获取字典的每一行
for password in passwords:
# 获取当前时间戳
times = int(time.time())
# 将时间戳进行md5加密
token = md5(str(times).encode('utf-8')).hexdigest()
data = {
"username" : "admin",
"password" : password.strip(),
"token" : token,
"submit" : '%E7%99%BB%E9%99%86'
}
# 发送post请求
resp = requests.post(url=url, data=data, headers=headers)
# 判断响应内容是否有 x1ongsec{ 有则表示成功
if 'x1ongsec{' in resp.text :
print('\033[32;1m[+] attack username={}&password={}&token={}&submit={}\033[0m'.format(data['username'], data['password'], data['token'], data['submit']))
print('\033[32;1m[+] username: {} password: {}'.format(data['username'], data['password']))
break
else :
print('\033[1;34m[*] attack username={}&password={}&token={}&submit={}\033[0m'.format(data['username'], data['password'], data['token'], data['submit']))

image-20240131215218542

得到密码登录即可。

返回在前端的 token

访问靶场出现如下页面:

image-20240201103246161

使用 burp 抓取登陆时的数据包,发现将用户名、密码和 token 值一并提交给服务器。

image-20240201103412991

查看前端代码,发现页面源码中存在隐藏的输入框,并且有默认值,每次刷新该值都会变。故而推断这是由服务端生成的 token 值然后返回在前端,而我们用户需要带着最新的 token 去请求服务器,不然会提示 Token Error

image-20240201103541891

重复利用的 token:

image-20240201103736150

那么我们就需要在每次 burp 暴破请求时,提取页面的 token 值一并提交给服务器,我们将该数据包发送到 Intruder 模块。

这里已知用户名为 admin ,因此用户名可固定,而随之变化的是 passwordtoken,于是将其设置为变量,并设置 pitchfork 模式。

image-20240201103855030

接着来到 Payloads 页面。先为密码设置字典:

image-20240201105034512

再为 token 设置为 Recursive grep 递归筛选

image-20240201105131332

接着来到 Settings 选项从页面中提取 token 值。框选 token 值即可自动生成表达式进行提取。

image-20240201105322024

最后点击 Start attack

image-20240201105410101

image-20240201105506161

最后得到密码 abcd1234

验证码识别

分析

访问靶场,出现如下页面:

image-20240201105659327

经过测试,发现必须输入正确的验证码且验证码不能重复利用,因此在 Less-4 的方法就不能使用了。

image-20240201105903769

这里需要使用的方法则是验证码识别。

环境准备

插件下载

这里我们使用到的验证码识别插件是captcha-killer-modified

Github 项目地址:https://github.com/f0ng/captcha-killer-modified

编译版本下载地址:https://github.com/f0ng/captcha-killer-modified/releases/download/0.21-beta/captcha-killer-modified-0.21-beta-jdk11.jar

项目下载

主要是用 codereg.py(需要提前准备python3的环境),且该项目源码包含用法常见问题文档。

Github 项目地址:https://github.com/f0ng/captcha-killer-modified/blob/main/codereg.py

源码如下:

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
68
69
70
71
72
73
# -*- coding:utf-8 -*-
# author:f0ngf0ng
# @Date: 2022/3/11 下午1:44
import argparse
import ddddocr
from aiohttp import web
import base64
print("欢迎使用captcha-killer-modified服务端脚本,项目地址:https://github.com/f0ng/captcha-killer-modified\n谷歌reCaptcha验证码 / hCaptcha验证码 / funCaptcha验证码商业级识别接口:https://yescaptcha.com/i/bmHz3C\n\n")
parser = argparse.ArgumentParser()

parser.add_argument("-p", help="http port",default=8888)
args = parser.parse_args()
ocr = ddddocr.DdddOcr()
port = args.p

auth_base64 = "f0ngauth" # 可自定义auth认证
# 识别常规验证码
async def handle_cb2(request):
if request.headers.get('Authorization') != 'Basic ' + auth_base64:
return web.Response(text='Forbidden', status='403')
# print(await request.text())
img_base64 = await request.text()
img_bytes = base64.b64decode(img_base64)
# return web.Response(text=ocr.classification(img_bytes)[0:4]) 验证码取前四位
# return web.Response(text=ocr.classification(img_bytes)[0:4].replace("0","o")) 验证码取前四位、验证码中的0替换为o
res = ocr.classification(img_bytes)
print(res)

return web.Response(text=ocr.classification(img_bytes)[0:10])

# 识别算术验证码
async def handle_cb(request):
zhi = ""
if request.headers.get('Authorization') != 'Basic ' + auth_base64:
return web.Response(text='Forbidden', status='403')
# print(await request.text())
img_base64 = await request.text()
img_bytes = base64.b64decode(img_base64)
res = ocr.classification(img_bytes).replace("=","").replace("?","")
print(res)
if'+'in res:
zhi = int(res.split('+')[0])+int(res.split('+')[1][:-1])
print(zhi)
return web.Response(text=str(zhi))
elif'-'in res:
zhi = int(res.split('-')[0])-int(res.split('-')[1][:-1])
print(zhi)
return web.Response(text=str(zhi))
elif'*'in res:
zhi = int(res.split('*')[0])*int(res.split('*')[1][:-1])
print(zhi)
return web.Response(text=str(zhi))
elif 'x' in res:
zhi = int(res.split('x')[0])*int(res.split('x')[1][:-1])
print(zhi)
return web.Response(text=str(zhi))
elif '/'in res:
zhi = int(res.split('/')[0])/int(res.split('/')[1][:-1])
return web.Response(text=str(zhi))
else:
return web.Response(text=res)



app = web.Application()
app.add_routes([
web.post('/reg2', handle_cb), # 识别算数验证码
web.post('/reg', handle_cb2), # 识别常规验证码
])

if __name__ == '__main__':

web.run_app(app, port=port)

项目依赖安装

在使用 codereg.py 之前,需要提前安装 python3 环境,并安装其对应的 pip

1
2
3
4
5
# pip
pip install -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com ddddocr aiohttp

# pip3
pip install -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com ddddocr aiohttp

项目报错解决

不出意外的话,你在运行脚本的时候会提示如下错误:

1
TypeError: The port is required to be int.

解决方法将11行的字符串 “8888” 修改为 int 类型的 8888,如下:

1
parser.add_argument("-p", help="http port",default=8888)

保存之后再次运行即可。

导入插件

在 burp 的工具栏中点击 Extensions -> Installed 中找到 Burp Extensions 点击 Add 选择插件路径(尽量不要包含中文路径)

image-20240201111228562

点击 Next ,最后点击 Close

注意:该插件仅支持高版本的 Burpsuite,低版本的请使用:captcha-killer.0.1.2.jar,具体自行百度。

识别工作

使用插件

首先获取验证码的生成地址,通常右击验证码点击检查即可在 src 中看到地址:

image-20240201111548253

访问该地址,然后 burp 抓包,将其发送到验证码识别插件:

image-20240201111647892

依次点击:

image-20240201111841954

该模版的默认接口URL 为 本地的 8080 端口,可以根据运行 codereg.py 的地址和端口自行修改。

image-20240201111930044

接着在本地运行 codereg.py 文件,启动接口。

image-20240201112105022

如果提示其他错误,请参考作者的 F&Q:https://github.com/f0ng/captcha-killer-modified/blob/main/FAQ.md

如果直接访问接口地址,可以提示 404 not found 属正常现象

项目启动成功之后,来到插件面板点击识别:

image-20240201112337733

据作者宣称识别率达到 85%,经过测试,发现识别率还是可以的。

验证码识别暴破

以上都完成之后,我们抓取登陆时的数据包:

image-20240201112623282

Attack Type 设置为 Pitchfork 然后将密码和验证码设置为变量。

image-20240201112705579

来到 Payloads 选项先设置密码字典:

image-20240201112843783

验证码字典则选择由插件生成:

image-20240201112924266

最后点击 Start attack 即可。由于验证码识别准确率只有 85% 左右,故而可能刚刚把正确密码的验证码识别错误了。故而可能需要多试几次。

而我这里刚好把正确密码的验证码输入错误了,故而错过了正确密码。

image-20240201113529626

不过这里发现了一点小技巧,将线程为1可以增加验证码的识别准确率:

image-20240201120007232

image-20240201120129577

最终得到密码为 Qwer1234!@#$

安全的后台管理系统

修复建议

对于暴力破解等漏洞的防护,如果从用户的角度如下:

  • 设置强密码
  • 定期更新密码
  • 避免多平台使用同一密码

如果从开发角度如下:

  • 注册用户时强制用户的密码必须包含大小写字母、数字等字符
  • 设置复杂的验证码
  • 如果有必要的情况下,建议从数据库层面将某个用户错误此处达到一定次数时锁定N分钟。

示例代码

本关卡主要为修复暴力破解漏洞,因此是从数据库层面,当用户错误此处达到3次,则锁定2分钟,2分钟后自动解锁。当然也有弊端,致使正常的用户也无法通过 admin 用户登录到网站。

image-20240201120635142

image-20240201120726943

源码如下:

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<?php
session_start();
define("DB_USER", "root");
define("DB_PASS", "root");
define("DB_HOST", "127.0.0.1");
define("DB_NAME", "houtai");

// 创建链接
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);

if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}

if (isset($_POST["submit"]) and !empty($_POST["username"]) and !empty($_POST["password"]) and !empty($_POST["code"])) {
if (strtolower($_POST["code"]) == strtolower($_SESSION["captcha_code"])) {
$in_username = $_POST["username"];
$in_password = md5($_POST["password"]);
$sql = "SELECT username,password FROM `users` WHERE username=? AND password=?";
// 预处理防止SQL注入
$sql = $conn->prepare($sql);
// 绑定参数
$sql->bind_param("ss", $in_username, $in_password);
// 绑定结果
$sql->bind_result($username, $password);
// 执行语句
$sql->execute();
// 将结果与变量进行绑定
$sql->fetch();
if ($in_username === $username && $in_password === $password) {
echo "<script>alert("登录成功!欢迎用户: {$username}")</script>";
echo "<script>window.location.replace(\"https://www.qwesec.com\");</script>";
} else {
// 用户身份验证失败,增加错误次数
increaseErrorCount($conn, $in_username);
// 设置解锁等待秒数
$lockSecond = 60;
// 检查错误次数是否达到3次,如果是则锁定账户
if (isAccountLocked($conn, $in_username,$lockSecond)) {
$sql = "SELECT lock_time FROM users WHERE username = "$in_username"";
$result = $conn->query($sql);
$row = $result->fetch_assoc();
$wait_time = intval($row["lock_time"]) - time();
echo "<script>alert(\"账户已锁定,请{$wait_time}秒后重试! \");</script>";
} else {
echo "<script>alert("用户名或密码错误!");</script>";
}
}
} else {
echo "<script>alert("验证码错误!");</script>";
}
}

// 增加错误次数

function increaseErrorCount($conn, $username)
{
$sql = "UPDATE `users` SET error_count = error_count + 1 WHERE username = "$username"";
$conn->query($sql);
}

// 检查账户是否已锁定
function isAccountLocked($conn, $username, $lockSecond)
{
$sql = "SELECT error_count,lock_time,is_locked FROM users WHERE username = "$username"";
$result = $conn->query($sql);

if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
$errorCount = $row["error_count"];
$lockTime = $row["lock_time"];
// 假设错误次数达到3次就锁定账户,你可以根据实际需求进行调整
if ($errorCount >= 3) {
// 如果当前时间大于锁定时间,则解锁账户
if ($lockTime !== NULl and time() > $lockTime) {
resetErrorCount($conn, $username);
return false;
} else if ($row["is_locked"] != 1){
// 锁定账户,设置锁定时间为5分钟(300秒)
$lockTime = time() + $lockSecond;
$sql = "UPDATE users SET is_locked = 1,lock_time = $lockTime WHERE username = "$username"";
$conn->query($sql);
return true;
}
return true;
}
}
}

// 重置错误次数和解锁状态
function resetErrorCount($conn, $username) {
$sql = "UPDATE users SET error_count = 0, is_locked = 0, lock_time = NULL WHERE username = "$username"";
$conn->query($sql);
}