DVWA学习之旅之Brute Force

Author: hxd
Date: 2025年4月11日

DVWA的简介

DVWA(Damn Vulnerable Web Application)一个用来进行安全脆弱性鉴定的PHP/MySQL Web 应用,旨在为安全专业人员测试自己的专业技能和工具提供合法的环境,帮助web开发者更好的理解web应用安全防范的过程。

DVWA 一共包含了十个攻击模块,分别是:Brute Force(暴力(破解))、Command Injection(命令行注入)、CSRF(跨站请求伪造)、- File Inclusion(文件包含)、File Upload(文件上传)、Insecure CAPTCHA (不安全的验证码)、SQL Injection(SQL注入)、SQL Injection(Blind)(SQL盲注)、XSS(Reflected)(反射型跨站脚本)、XSS(Stored)(存储型跨站脚本)。包含了 OWASP TOP10 的所有攻击漏洞的练习环境,一站式解决所有 Web 渗透的学习环境。

另外,DVWA 还可以手动调整靶机源码的安全级别,分别为 Low,Medium,High,Impossible,级别越高,安全防护越严格,渗透难度越大。

一般 Low 级别基本没有做防护或者只是最简单的防护,很容易就能够渗透成功;而 Medium 会使用到一些非常粗糙的防护,需要使用者懂得如何去绕过防护措施;High 级别的防护则会大大提高防护级别,一般 High 级别的防护需要经验非常丰富才能成功渗透;

最后 Impossible 基本是不可能渗透成功的,所以 Impossible 的源码一般可以被参考作为生产环境 Web 防护的最佳手段。

下载和搭建过程我就省略了,网上都有,下面让我们直接开始:

Burp Force (暴力破解)

Low Level(开发人员完全忽视了任何保护方法,允许任何人多次任意访问,可以在没有任何影响的情况下对任意用户进行登录)

源码审计

<?php

if( isset( $_GET[ 'Login' ] ) ) {
// Get username
$user = $_GET[ 'username' ];

// Get password
$pass = $_GET[ 'password' ];
$pass = md5( $pass );

// Check the database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];

// Login successful
echo "<p>Welcome to the password protected area {$user}</p>";
echo "<img src=\"{$avatar}\" />";
}
else {
// Login failed
echo "<pre><br />Username and/or password incorrect.</pre>";
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

源码如下,代码将获取用户输入的用户名和密码并将其进行 md5 加密,然后使用 SQL SELECT 语句进行查询。由于进行了 md5 加密,因此直接阻止了 SQL 注入,因为经过 md5 这种摘要算法之后 SQL 语句就会被破坏(不过这里用 SQL 注入可以登陆成功)。注意到此时服务器只是使用了 isset() 函数验证了参数 Login 是否被设置,参数 username、password 没有做任何过滤,更重要的是没有任何的防爆破机制。

Gesture

首先打开BP抓包,使用Sniper狙击手模式,只更改password,同时加载Payload,如下:

image-20250411171446127

爆破如下,发现password长度不一样:

image-20250411171528862

说明Low Level的用户名是admin,密码是password,登陆如下:

image-20250411171659702

Medium Level(此阶段在验证失败的登陆屏幕上添加睡眠,这意味着当你登录不正确时,在页面可见之前将有两秒钟的等待。这只会减慢一分钟内可处理的请求量,使暴力攻击的时间更长)

代码审计

<?php

if( isset( $_GET[ 'Login' ] ) ) {
// Sanitise username input
$user = $_GET[ 'username' ];
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Sanitise password input
$pass = $_GET[ 'password' ];
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );

// Check the database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];

// Login successful
echo "<p>Welcome to the password protected area {$user}</p>";
echo "<img src=\"{$avatar}\" />";
}
else {
// Login failed
sleep( 2 );
echo "<pre><br />Username and/or password incorrect.</pre>";
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

源码如下,Medium 级别的代码主要增加了 mysql_real_escape_string 函数,该函数会对字符串中的特殊符号进行转义,从而对用户输入的参数进行了简单的过滤。相比 low 级别的代码,当登录验证失败时界面将冻结 2 秒,从而影响了爆破操作的效率,不过如果是一个闲来无事并且很有耐心的白帽黑客,爆破出密码仍然是时间问题。

Gesture

继续使用BP抓包,跟Low Level一样的操作,只是由于登录验证失败时界面会冻结2秒,所以会慢一点:

image-20250411173801600

发现密码还是password:

image-20250411173926027

成功登录:

image-20250411174001014


High Level(开发者使用了”CSRF”的反伪造请求,有一个旧的说法表示这种保护可以阻止暴力攻击,但事实并非如此。这个级别扩展了中等级别,在登录失败时等待,但这次是 2 到 4 秒之间的随机时间,这样做的目的是试图混淆任何时间预测。使用验证码表单可能会产生与 CSRF 令牌类似的效果。)

代码审计

<?php

if( isset( $_GET[ 'Login' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Sanitise username input
$user = $_GET[ 'username' ];
$user = stripslashes( $user );
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Sanitise password input
$pass = $_GET[ 'password' ];
$pass = stripslashes( $pass );
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );

// Check database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];

// Login successful
echo "<p>Welcome to the password protected area {$user}</p>";
echo "<img src=\"{$avatar}\" />";
}
else {
// Login failed
sleep( rand( 0, 3 ) );
echo "<pre><br />Username and/or password incorrect.</pre>";
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

// Generate Anti-CSRF token
generateSessionToken();

?>

High 级别的代码使用了stripslashes 函数,进一步过滤输入的内容。同时使用了 Token 抵御 CSRF 攻击,在每次登录时网页会随机生成一个 user_token 参数,在用户提交用户名和密码时要对 token 进行检查再进行 sql 查询。

Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码

写一段python脚本查询:

import requests
from bs4 import BeautifulSoup
r=requests.get("http://192.168.239.1/DVWA/vulnerabilities/brute/?username=admin&password=password&Login=Login&user_token=a9427c593c12feca4bf502f3106af547#")
demo=r.text
soup=BeautifulSoup(demo,"html.parser")
print(soup.prettify())

发现每次的user_token都是不一样的,如下:

image-20250411203400372

image-20250411203412676

这里有两种思路:

  • 第一种方法:

    也是先使用BP抓包,然后我们使用Pitchfork模式,为password和token添加变量进行爆破,如下:

    image-20250411210135432

第一个username导入本地爆破字典,然后第二个token我们打开右边的setting找到Grep-Extract点击add添加

Grep-Extract的基本用法

Grep-Extract功能允许用户通过正则表达式来匹配和提取HTTP请求或响应中的特定数据。用户可以在Burp Suite的Intruder模块中使用Grep-Extract,通过设置特定的正则表达式来过滤和提取需要的数据。例如,在SQL注入测试中,可以使用Grep-Extract来提取数据库中的表名或列名‌

image-20250411211035463

上述的value值等会自动生成,然后点击OK。在Payloads加载界面为token变量设置Payload type为Recursive grep递归模式,同时设置initial payload for first request要为刚刚在Grep-Extract中的token值,如下:

image-20250411214032846

然后爆破找到密码为password:

image-20250411213953437

然后成功登录:

image-20250411214109324

附录

Low Level

✅ 正常功能部分

if( isset( $_GET[ 'Login' ] ) ) {
  • 如果 URL 中设置了 Login 参数,就执行登录流程。
$user = $_GET[ 'username' ];
$pass = $_GET[ 'password' ];
$pass = md5( $pass );
  • 从 GET 参数中获取用户名和密码,并用 md5 对密码进行哈希(这已经过时,后面详说)。
$query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
  • 构造 SQL 查询语句来验证用户身份。
$result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( ... );
  • 使用 mysqli 查询数据库,如果失败就打印错误。
if( $result && mysqli_num_rows( $result ) == 1 ) {
  • 如果查到一个匹配的用户,就说明登录成功。
$row    = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];

echo "<p>Welcome to the password protected area {$user}</p>";
echo "<img src=\"{$avatar}\" />";
  • 显示欢迎信息和头像。
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
  • 关闭数据库连接。

⚠️ 存在的安全问题分析

1. 🚨 SQL 注入漏洞

$query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
  • 直接把用户输入拼接进 SQL 语句,容易被 SQL 注入攻击。

  • 示例攻击:

    username=admin' --&password=123

    这会让原始 SQL 查询变成:

    SELECT * FROM `users` WHERE user = 'admin' --' AND password = 'md5hash';

    -- 表示注释,后面被忽略,导致密码验证被绕过。

✅ 修复方法:

使用 预处理语句(prepared statements)

$stmt = $mysqli->prepare("SELECT * FROM users WHERE user = ? AND password = ?");
$stmt->bind_param("ss", $user, $pass);

2. 🔒 弱密码哈希(使用了 md5)

  • md5 已被认为不安全,容易被彩虹表或暴力破解。

  • 应使用 password_hash()password_verify() 进行密码加密与校验:

// 注册时保存:
$password_hash = password_hash($password, PASSWORD_DEFAULT);

// 登录时验证:
if (password_verify($input_password, $password_hash)) { ... }

3. 📥 使用 GET 提交密码

$_GET['password']
  • 密码作为 URL 参数发送,很容易泄露,例如:

    • 浏览器历史记录

    • 日志文件

    • 网络嗅探

✅ 应使用 POST 提交表单数据,并通过 $_POST 获取。


4. 📤 XSS(跨站脚本)风险

echo "<p>Welcome to the password protected area {$user}</p>";
  • 如果用户名中有 HTML/JS 代码,会被直接输出,导致 XSS。

✅ 使用 htmlspecialchars() 进行转义:

echo "<p>Welcome to the password protected area " . htmlspecialchars($user) . "</p>";

🔚 总结:这段代码的问题汇总

问题 风险级别 修复建议
SQL 注入 使用预处理语句
使用 md5 加密密码 使用 password_hash
GET 提交敏感信息 使用 POST 方法
没有转义用户输出(XSS) 使用 htmlspecialchars
弱错误处理方式 日志记录+用户友好提示

Medium Level

这段代码相比你上一次发的版本,做了一些改进,主要是增加了对用户输入的转义处理(mysqli_real_escape_string,来减缓 SQL 注入的风险,并在登录失败时加入了一个 sleep(2) 延迟来防止暴力破解。但仍然存在很多安全问题和过时的做法。我们继续逐点分析:


✅ 改进的地方

1. 使用 mysqli_real_escape_string 对用户名和密码进行转义

$user = mysqli_real_escape_string($conn, $_GET['username']);
$pass = mysqli_real_escape_string($conn, $_GET['password']);

✅ 这比直接拼接更安全,防止一些基本的 SQL 注入攻击。


2. 登录失败加了 sleep(2)

sleep( 2 );

✅ 这是一个很基础的反暴力破解保护,防止脚本快速尝试密码。但仅靠这个远远不够。


❌ 仍然存在的问题和风险

1. ❗ SQL 注入风险仍然存在

虽然用了 mysqli_real_escape_string,但这不是完全防护,尤其是面对复杂或绕过型注入。正确的做法是使用预处理语句(prepared statements)

✅ 正确用法示例:

$stmt = $mysqli->prepare("SELECT * FROM users WHERE user = ? AND password = ?");
$stmt->bind_param("ss", $user, $pass);

2. ❗ 使用 md5 加密密码 —— 非常不安全

$pass = md5($pass);
  • md5 已被完全淘汰,能被秒破,甚至有公开反查库。

  • 应使用 password_hash() + password_verify()

✅ 替代方式:

// 注册时存入:
$hashed_password = password_hash($password, PASSWORD_DEFAULT);

// 登录时验证:
if (password_verify($input_password, $hashed_password)) { ... }

3. ❗ 仍使用 GET 请求传递敏感信息

$_GET['username'], $_GET['password']
  • 使用 GET 会将用户名和密码暴露在 URL 中(浏览器历史、日志、代理服务器都会记录)。

  • 应使用 POST 提交表单。

✅ 用法:

<form method="POST" action="login.php">
$_POST['username']

4. ❗ 输出未经过转义,存在 XSS 风险

echo "<p>Welcome to the password protected area {$user}</p>";
  • 如果用户名是 <script>alert('XSS')</script>,就会被浏览器执行。

✅ 使用 htmlspecialchars 转义输出:

echo "<p>Welcome to the password protected area " . htmlspecialchars($user, ENT_QUOTES, 'UTF-8') . "</p>";

5. ❗ 不安全的数据库错误信息暴露

die( '<pre>' . mysqli_error(...) . '</pre>' );
  • 把数据库错误暴露给用户是一个安全隐患。攻击者可以据此构造攻击。

  • 正确做法是:记录日志,展示通用错误信息。


✅ 总结与建议

问题/改进 现状 建议
SQL 注入防护 使用了 mysqli_real_escape_string,但还不够 ✅ 使用 prepared statements
密码哈希 使用 md5(不安全) ✅ 改用 password_hash / password_verify
请求方式 使用 GET(不安全) ✅ 改用 POST
XSS 防护 用户输出未转义 ✅ 使用 htmlspecialchars()
错误信息 暴露数据库错误 ✅ 隐藏详细错误,只写日志
暴力破解防护 使用 sleep(2) ✅ 可以进一步引入登录失败计数机制

High Level

这段 PHP 代码是一个带有 CSRF 防护、基本 SQL 查询和登录逻辑 的登录处理脚本。它在你前面发的版本基础上,又增加了一些安全性,比如:

  • ✅ 增加了 CSRF Token 检查

  • ✅ 添加了 stripslashes() 去除魔术引号(在老版本 PHP 中可能有用)

  • ✅ 登录失败时加入了 随机 sleep 时间 来抵抗爆破

  • ❌ 但仍然存在几个关键的安全问题


🧩 一步步分析

✅ 1. 检查是否提交了登录请求

if( isset( $_GET[ 'Login' ] ) ) {
  • 表示是一个通过 GET 请求提交的登录表单(不推荐,敏感信息暴露在 URL 中)

✅ 2. CSRF Token 检查

checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
  • 这是对跨站请求伪造(CSRF)的防护。

  • user_token 是表单提交的,session_token 是服务端生成的。

  • 如果验证失败,通常会重定向或退出(checkToken() 函数你没贴出来,但应该做了这些)。


✅ 3. 输入处理:去除转义 + SQL 转义

$user = stripslashes( $_GET[ 'username' ] );
$user = mysqli_real_escape_string( ..., $user );

$pass = stripslashes( $_GET[ 'password' ] );
$pass = mysqli_real_escape_string( ..., $pass );
$pass = md5( $pass );
  • stripslashes():去掉魔术引号(PHP 5.x 遗留问题)

  • mysqli_real_escape_string():防止基本 SQL 注入

  • md5($pass)非常不安全,见下文


✅ 4. 构造并执行 SQL 查询

$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query(...);
  • 这是典型的用户名 + 密码校验语句

  • ❌ 使用了拼接 SQL 语句而不是参数化查询(仍有注入风险


✅ 5. 判断登录是否成功

if( $result && mysqli_num_rows( $result ) == 1 ) {
  • 如果只有一条记录,表示用户名和密码匹配

  • 成功时显示头像并欢迎用户

echo "<img src=\"{$avatar}\" />";

未经过 htmlspecialchars() 转义,仍有 XSS 风险


✅ 6. 登录失败处理

sleep( rand( 0, 3 ) );
  • ✅ 添加了随机延迟,降低爆破攻击效率

✅ 7. 生成 CSRF Token

generateSessionToken();
  • 表示页面加载时会为用户创建一个随机的 session token

  • 页面上应该会包含 <input type="hidden" name="user_token" value="...">


❗ 安全问题总结与建议

问题 解释 建议
❌ 使用 GET 提交表单 用户名、密码会暴露在 URL ✅ 改用 POST
❌ 使用 md5 存储密码 不安全,易被破解 ✅ 使用 password_hashpassword_verify
❌ SQL 拼接 即使转义了,仍可能有注入风险 ✅ 使用 prepared statements
❌ 输出未转义 用户名或头像路径可被注入 JS 脚本 ✅ 使用 htmlspecialchars()
⚠ 依赖老函数(如 stripslashes) 可能说明 PHP 配置问题 ✅ 建议检查 magic_quotes_gpc 是否关闭