文章目录
前言
DVWA(全称 Damn Vulnerable Web Application)是一个专门为安全人员、渗透测试人员、网络安全爱好者设计的练习靶场。
它是一个搭建在 Web 服务器上的超弱安全性的网站,里面故意做了很多漏洞,供人练习攻击和防护技巧。
简单说,就是一个故意很脆弱的网站,让你安全地、合法地去学 Web 攻击和防御!
接下来是他不同等级的攻略方式和思路,仅供参考。以下方法我均使用的bp自带的浏览器。
一、Brute Force
Brute Force(暴力破解)是一种最简单且直观的密码破解方法,其原理是通过尝试所有可能的密码组合,直到找到正确的密码。暴力破解不依赖任何智能猜测或算法优化,而是通过穷举法逐一测试所有可能的密码或密钥。
其基本原理如下:
-
密码空间:攻击者首先需要知道密码的长度和可能的字符集(例如,字母、数字、特殊字符等)。这些信息决定了暴力破解所需尝试的所有可能密码的数量。
-
穷举法:攻击者会从所有可能的密码组合开始尝试,逐个尝试直到匹配正确的密码。例如,如果密码是4位数字,且只使用数字字符(0-9),那么密码空间就是从0000到9999共10,000种可能,暴力破解就会从0000开始,依次尝试所有组合。
-
破解方式:暴力破解是完全依赖于计算机的处理能力。计算机可以在短时间内通过并行化或更强的计算能力迅速完成大量密码尝试。
这里猜解之后,回车之前,打开burpsuite抓包
抓包成功之后如下图:可以看到这里的传参方式是get方法传参,
GET
方法传参的基本原理:
当你使用GET
方法向服务器发送请求时,参数会以键=值
的形式附加在URL后面。例如:
http://example.com/search?q=python&sort=desc
典型用途:
-
查询操作:常用于表单提交后,查询特定信息,例如搜索引擎的查询。
-
分页操作:通过在URL中传递页码信息来实现分页。
-
筛选条件:传递不同的筛选条件(例如排序、分类等)以影响服务器返回的结果。
GET
方法传参的特点:
-
可见性:参数直接显示在URL中,因此它们对用户是可见的,不适合传递敏感数据。
-
长度限制:URL的长度有限制,通常最大约为2048个字符,因此传递的参数数量或长度受限。
-
幂等性:
GET
请求通常是幂等的,意味着相同的GET
请求会产生相同的结果,且不会对服务器的状态造成副作用。
将抓取到的数据包发送给攻击模块intruder:
点击action===》send to intruder
选择攻击模式,攻击模式有四种:
1. Sniper Attack(狙击手):
-
功能:Sniper 攻击是一种单点攻击模式,主要用于测试单个参数的漏洞。攻击者逐一对目标中的每个单独参数进行攻击。
-
使用场景:适用于暴力破解或枚举一个参数的不同值(例如用户名、密码或其他输入字段)。
-
举例:在登录表单中,Sniper 攻击会逐一尝试每个可能的用户名和密码组合。
2. Battering Ram Attack(攻城锤):
-
功能:Battering Ram 攻击模式将相同的攻击负载应用到多个参数上。该模式通过对所有指定参数使用相同的攻击数据来进行测试。
-
使用场景:适用于所有参数应该具有相同值的情况,例如在多个字段中输入相同的密码或令牌进行暴力破解。
-
举例:在一个多字段的表单中,Battering Ram 攻击会对每个字段使用相同的密码进行测试,适用于密码重置表单等情况。
3. Pitchfork Attack(叉草):
-
功能:Pitchfork 攻击模式用于同时攻击多个参数,但每个参数都使用不同的攻击数据。多个攻击字典可以同时用于多个参数。
-
使用场景:适用于需要同时用多个词典对多个参数进行测试的情况。例如,用户的用户名和密码可以分别来自两个不同的字典。
-
举例:在登录页面,Pitchfork 攻击可以分别使用一个字典对用户名字段进行攻击,并使用另一个字典对密码字段进行攻击。
4. Cluster Bomb Attack(集束炸弹):
-
功能:Cluster Bomb 攻击模式通过对多个参数使用不同的字典组合进行攻击。每个参数都会分别使用不同的词典,产生所有可能的组合。
-
使用场景:适用于多个字段中需要尝试所有可能组合的情况。它比 Pitchfork 攻击更复杂,因为它会生成所有的组合。
-
举例:在一个表单中,Cluster Bomb 攻击可能会将字典1应用于用户名字段,将字典2应用于密码字段,并将字典3应用于邮箱字段,然后生成所有可能的组合。
我们这里属于多个参数使用不同的字典组合进行攻击。每个参数都会分别使用不同的词典,产生所有可能的组合。所以选择集束炸弹,点击payload,这里的payload是有效负载的意思,这里的有效负载更多的是指:通过自己构建的参数或者脚本达到渗透测试目的的一切手段,都叫payload
1)用Brute forcer类型来爆破的特点
-
全面性:
-
所有可能的组合:Brute Force 的核心特点是尝试所有可能的密码组合(或者其他凭证)。它不会跳过任何一组密码,只要密码长度和字符集被配置好,爆破工具会遍历每一个可能的组合,直到找到正确的密码。
-
适用于密码破解、令牌破解、密钥破解等场景。
-
-
暴力破解:
-
无智能判断:Brute Force 是一种“暴力”方式,缺乏任何智能判断或优化,它依靠大量的计算力尝试每一个组合。无论密码是否符合一定的规则,都会尝试所有可能的情况。
-
这意味着它是最原始的密码破解方法,通常非常耗时。
-
-
高资源消耗:
-
时间长:由于暴力破解要尝试所有组合,所以它的速度相对较慢,特别是当密码长度较长或字符集较大时。攻击者需要不断发送请求并等待服务器响应,这对于计算资源和时间都是巨大的消耗。
-
服务器负载:在进行暴力破解时,由于大量的请求,可能会给目标服务器带来较大的负载,进而引起性能问题,甚至触发防护机制(如限流、封禁IP等)。
-
-
无法绕过防护机制:
-
防护绕过困难:如果目标应用程序有一些防护机制(例如验证码、账号锁定、IP封禁等),Brute Force 攻击可能很难成功。特别是现代Web应用常常加入了各种防护措施,Brute Force 在这些情况下会变得不太有效。
-
例如,如果目标系统在多次失败后锁定账户或启用验证码,攻击者的请求会被中断或限制。
-
-
适用场景:
-
简单的密码验证系统:Brute Force 适用于没有启用安全机制(如限制登录尝试次数、启用验证码等)的系统,或者当密码相对较短、字符集有限时。
-
字典攻击补充:Brute Force 还可以作为字典攻击的补充,用于暴力破解那些不在字典中的密码组合。
-
-
成功率:
-
最终能够破解:理论上,Brute Force 的成功率是100%,因为它会尝试每个可能的密码。然而,由于其高时间成本和对防护机制的依赖,它通常不是最优的攻击方式,尤其对于复杂的密码来说。
-
2)Simple List 爆破方式是指使用一个简单的列表(通常是预定义的字典或字符串集合)进行暴力破解。与 Brute Force 攻击方式不同,Simple List 仅尝试列表中存在的特定密码或凭证,而不是穷举所有可能的组合。这种方式通常被用作字典攻击的一部分,通过快速尝试一个相对较小的、常见的密码或凭证列表,来判断目标系统的密码是否在其中。
Simple List 爆破方式的特点:
-
速度较快:
-
有限的尝试范围:由于只使用预定义的密码列表(通常是字典),Simple List 爆破比 Brute Force 更快,因为它不需要穷举所有可能的字符组合,只需测试一个相对较小的字典集合。
-
效率较高:如果目标系统使用了常见的密码或弱密码,Simple List 爆破方式能够在较短的时间内成功。
-
-
依赖于密码字典:
-
有效性依赖字典质量:Simple List 爆破方式的成功率很大程度上取决于所使用字典的质量。如果字典包含了目标用户可能使用的密码,攻击的成功率会较高。如果字典不包含正确的密码,则攻击将失败。
-
常见密码:字典通常包含大量常见密码、常见密码变种、或是泄漏的密码集合,因此 Simple List 更适用于面对使用弱密码的目标。
-
-
资源消耗较低:
-
较低的计算资源消耗:与 Brute Force 相比,Simple List 爆破的计算资源消耗较低,因为它只需要尝试字典中的有限几个选项,不会浪费大量时间和计算在不可能的密码组合上。
-
-
适用场景:
-
弱密码:Simple List 爆破方式特别适用于攻击那些使用弱密码、易于猜测的系统(例如,使用“123456”、“password”或“admin”等简单密码的账户)。
-
字典攻击:常常与字典攻击结合使用,特别是在渗透测试中,字典攻击通过对常见密码的测试,能够快速识别系统中常见的密码漏洞。
-
-
不适合复杂密码:
-
对复杂密码无效:对于使用强密码(包含随机字符、大小写字母、数字和特殊符号)的账户,Simple List 爆破通常无法成功,因为字典中不包含这些复杂密码。
-
对抗性较强:如果目标系统有一些安全防护(例如账户锁定、验证码等),Simple List 爆破的效果会降低,因为字典攻击一般较为快速,可能会触发这些防护机制。
-
-
低隐蔽性:
-
快速且易被发现:由于字典攻击非常快速,攻击者往往在短时间内就会尝试大量常见的密码,这使得目标系统很容易检测到异常的登录请求并采取防护措施(如IP封禁、账户锁定等)。
-
优点:
-
速度快:相较于 Brute Force,Simple List 爆破速度更快,因为它只尝试字典中的密码。
-
资源消耗低:相比于暴力破解所有可能的组合,Simple List 爆破的资源消耗较低。
-
容易实现:通过使用现成的字典列表,攻击者可以轻松进行字典攻击,无需自己生成密码组合。
缺点:
-
成功率依赖字典:如果密码不在字典中,攻击就会失败,因此这种方法对于复杂和不常见的密码无效。
-
对强密码无效:如果目标系统采用了较强的密码策略,Simple List 攻击几乎无法破解密码。
-
易被防护机制发现:如果目标应用有防护措施(如账户锁定、限制登录尝试次数等),字典攻击会很容易触发这些防护,导致攻击失败。
总结:
Simple List 爆破方式的主要特点是速度较快、资源消耗较低,并且依赖于预定义的密码列表(字典)。这种方式适用于尝试常见的弱密码,但对于复杂密码或有防护措施的系统来说,它的成功率较低。它是一种高效、但相对简化的爆破方法,适合用于暴力破解那些容易猜测的密码。
第一个payload的自定义字典如下,可以自行猜解一些用户名
第二个参数的猜解如下,没猜一个密码回车就行,会自动占一行
点击开始攻击start attack,然后多次点击length排序,找出与其他返回长度不同那个值,对应的payload1,2就是正确的用户名和密码
这里能看到正确的用户名为:admin 密码为:password
到dvwa登录验证
源码分析:
Brute Force Source
vulnerabilities/brute/source/low.php
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// 获取用户名
$user = $_GET[ 'username' ]; // 从 GET 请求中获取 'username' 参数值
// 获取密码
$pass = $_GET[ 'password' ]; // 从 GET 请求中获取 'password' 参数值
$pass = md5( $pass ); // 使用 MD5 对密码进行加密(已过时,不推荐使用)
// 查询数据库,检查用户名和密码是否匹配
$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 ) {
// 如果查询成功且返回的行数为 1,表示登录成功
$row = mysqli_fetch_assoc( $result ); // 获取查询结果的一行数据
$avatar = $row["avatar"]; // 从结果中获取用户的头像
// 登录成功,显示欢迎信息及用户头像
echo "<p>Welcome to the password protected area {$user}</p>";
echo "<img src=\"{$avatar}\" />"; // 显示用户头像
}
else {
// 如果查询失败或没有匹配的记录,表示登录失败
echo "<pre><br />Username and/or password incorrect.</pre>";
}
// 关闭数据库连接
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
将dvwa的安全级别设置为medium
还是用于刚才相同的方式进行爆破,区别在于,点击start attack后,后发现爆破的速度明显下降,耗时明显增加
分析源码可知原因
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// 对用户名输入进行过滤,防止 SQL 注入
$user = $_GET[ 'username' ]; // 从 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)) ? "" : ""));
// 使用 mysqli_real_escape_string 对用户输入的用户名进行转义,以防止 SQL 注入(防止特殊字符被误解为 SQL 语句的一部分)
// 对密码输入进行过滤,防止 SQL 注入
$pass = $_GET[ 'password' ]; // 从 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)) ? "" : ""));
// 使用 mysqli_real_escape_string 对用户输入的密码进行转义
$pass = md5( $pass ); // 对密码进行 MD5 加密(已过时,不推荐使用)
// 查询数据库,检查用户名和密码是否匹配
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
// 这里仍然存在 SQL 注入漏洞,虽然使用了 mysqli_real_escape_string 进行部分过滤,但直接将变量拼接在 SQL 查询中,攻击者仍然可以通过某些方法绕过过滤。
$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 ) {
// 如果查询成功且返回的行数为 1,表示登录成功
$row = mysqli_fetch_assoc( $result ); // 获取查询结果的一行数据
$avatar = $row["avatar"]; // 从结果中获取用户的头像
// 登录成功,显示欢迎信息及用户头像
echo "<p>Welcome to the password protected area {$user}</p>";
echo "<img src=\"{$avatar}\" />"; // 显示用户头像
}
else {
// 如果查询失败或没有匹配的记录,表示登录失败
sleep( 2 ); // 稍作延迟,防止暴力破解工具进行快速重试
echo "<pre><br />Username and/or password incorrect.</pre>"; // 输出“用户名和/或密码错误”的消息
}
// 关闭数据库连接
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
两个源码的主要区别在于以下几点:
-
增加了
sleep(2)
延迟:-
在第二个源码中,登录失败时加入了
sleep(2)
,这会使系统在登录失败时延迟 2 秒响应。这是为了减缓暴力破解工具的速度,防止恶意脚本进行快速反复的猜测。 -
在第一个源码中,并没有这样的延迟,导致暴力破解工具能够更快速地进行尝试。
-
-
输入过滤的改进:
-
第二个源码中对用户输入的用户名和密码都使用了
mysqli_real_escape_string()
进行了过滤,以防止 SQL 注入攻击。这个改动相对于第一个源码来说是一个安全性上的改善。 -
第一个源码中的查询没有进行任何过滤,直接将用户输入的数据拼接到 SQL 查询中,导致存在明显的 SQL 注入漏洞。
-
-
整体功能结构:
-
除了上述差异外,两个源码的核心逻辑几乎相同,都是基于
$_GET
获取用户名和密码,执行 SQL 查询,检查数据库中的用户信息,最后根据查询结果进行登录成功或失败的反馈。
-
总结:
-
主要的区别是第二个源码在登录失败时增加了延迟(
sleep(2)
),并且对用户输入进行了过滤(使用mysqli_real_escape_string()
)。这两点增强了系统的安全性,尤其是防止暴力破解攻击。
将dvwa的安全级别设置为高级
继续使用相同方式去爆破
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// 通过调用 checkToken 函数,检查请求中的 user_token 和服务器端会话中的 session_token 是否匹配,以防止 CSRF 攻击。
// Sanitise username input
$user = $_GET[ 'username' ];
$user = stripslashes( $user );
// 使用 stripslashes 函数去除字符串中的反斜杠(防止输入时有多余的转义字符)。
$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)) ? "" : ""));
// 使用 mysqli_real_escape_string 对用户名进行转义,防止 SQL 注入攻击。
// Sanitise password input
$pass = $_GET[ 'password' ];
$pass = stripslashes( $pass );
// 使用 stripslashes 函数去除密码中的反斜杠(防止输入时有多余的转义字符)。
$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)) ? "" : ""));
// 使用 mysqli_real_escape_string 对密码进行转义,防止 SQL 注入攻击。
$pass = md5( $pass );
// 对密码进行 MD5 加密(尽管 MD5 已不再安全,但依然被用于这段代码中)。
// Check database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
// 构造 SQL 查询,验证用户名和密码是否正确。
$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>' );
// 执行 SQL 查询,若查询失败,则输出错误信息。
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 ) );
// 登录失败时,随机等待 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();
?>
// 生成 Anti-CSRF token,确保每个会话都有一个唯一的标识,用于防止 CSRF 攻击。
与第二个版本相比,这个源码做了以下几项优化和改进:
1. 增加了 Anti-CSRF Token 校验
-
在当前版本中,增加了对 CSRF 攻击的防护。通过
checkToken
函数检查请求中的user_token
是否与服务器端会话中的session_token
匹配。如果不匹配,则请求将被拒绝。这防止了跨站请求伪造(CSRF)攻击。 -
这一部分的代码是当前版本与第二个版本的主要区别之一,增强了系统的安全性。
2. 改进了输入数据的过滤
-
在当前版本中,除了使用
mysqli_real_escape_string
进行 SQL 注入防护外,还在用户名和密码输入时添加了stripslashes
函数,去除反斜杠字符。这对于防止攻击者利用反斜杠注入恶意代码或构造恶意输入有一定的帮助。
3. 随机延时
-
在登录失败时,当前版本通过
sleep( rand( 0, 3 ) )
随机延迟 0 到 3 秒之间的时间,进一步增加暴力破解的难度。随机延时机制使得攻击者无法通过自动化脚本快速测试多个密码组合,提升了系统的抗暴力破解能力。 -
第二个版本使用了
sleep( 2 )
,这只是固定延时,而当前版本通过随机延时,增加了更多的不确定性,使得攻击更加困难。
4. 生成 Anti-CSRF Token
-
当前版本最后调用了
generateSessionToken()
函数,这将生成一个新的会话令牌,并可能将其存储在用户的会话中。这样,每次用户请求时都会有一个新的、唯一的 CSRF token,这提高了防止 CSRF 攻击的效果。 -
这个改动是当前版本引入的一个额外的安全措施,在第二个版本中并没有这个功能。
总结
-
当前版本相较于第二个版本,增强了 防止 CSRF 攻击、输入过滤 和 防止暴力破解 的能力。特别是通过引入 Anti-CSRF Token 校验 和 随机延时,提高了系统的安全性。
来到高级、开启拦截抓包,可以多了一个user_token,这个token值每次登录都会发生变化,所以后面的操作稍有不同
将用户确定为admin,另外2个变量都add添加好,设置叉草攻击模式
第二个token参数设置的时候,先选择settings设置,设置参数提取,即每次都要提取数据包中的token值
创建资源池,设置当前最大请求数为1
在settings中设置最大失败的尝试数为1
打开重新定向
获取当前数据包相应之后,输入token搜索(如果没有前面的重定向always勾选)这里相应包里面是一个302的临时重定向
选中之后点击OK
设置第二个payload参数,选择payload类型为递归查找,也就是找每次的token值作为这个payload,然后点开始攻击
点击length排序、对于没有token值的那些返回,不用看,在有token值的返回中,找唯一不一样的那个,那说明这个是登录成功的,得到密码是password
分析一下impossible级别的代码
<?php
// 如果收到了登录表单提交(按钮Login被按下,且用户名和密码都填写了)
if (isset($_POST['Login']) && isset($_POST['username']) && isset($_POST['password'])) {
// 校验Anti-CSRF令牌,防止跨站请求伪造
checkToken($_REQUEST['user_token'], $_SESSION['session_token'], 'index.php');
// 获取并处理用户名输入
$user = $_POST['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)) ? "" : ""));
// 获取并处理密码输入
$pass = $_POST['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); // 将密码进行MD5加密(注意:实际开发中MD5已不安全)
// 定义默认值
$total_failed_login = 3; // 最大失败次数
$lockout_time = 15; // 锁定时间(分钟)
$account_locked = false; // 是否被锁定
// 查询数据库,检查用户登录失败的次数和上次登录时间
$data = $db->prepare('SELECT failed_login, last_login FROM users WHERE user = (:user) LIMIT 1;');
$data->bindParam(':user', $user, PDO::PARAM_STR);
$data->execute();
$row = $data->fetch();
// 判断该用户是否已被锁定
if (($data->rowCount() == 1) && ($row['failed_login'] >= $total_failed_login)) {
// 如果失败次数超过了限制,开始计算解锁时间
$last_login = strtotime($row['last_login']); // 转换上次登录时间
$timeout = $last_login + ($lockout_time * 60); // 允许再次登录的时间点
$timenow = time(); // 当前时间
// 如果当前时间小于解锁时间,说明账号仍然被锁定
if ($timenow < $timeout) {
$account_locked = true;
}
}
// 查询数据库,验证用户名和密码是否匹配
$data = $db->prepare('SELECT * FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;');
$data->bindParam(':user', $user, PDO::PARAM_STR);
$data->bindParam(':password', $pass, PDO::PARAM_STR);
$data->execute();
$row = $data->fetch();
// 如果验证成功且账号未被锁定
if (($data->rowCount() == 1) && ($account_locked == false)) {
// 获取用户详情
$avatar = $row['avatar'];
$failed_login = $row['failed_login'];
$last_login = $row['last_login'];
// 显示欢迎信息和头像
echo "<p>Welcome to the password protected area <em>{$user}</em></p>";
echo "<img src=\"{$avatar}\" />";
// 如果用户之前账号曾因多次失败被锁定,给出提示
if ($failed_login >= $total_failed_login) {
echo "<p><em>Warning</em>: Someone might have been brute forcing your account.</p>";
echo "<p>Number of login attempts: <em>{$failed_login}</em>.<br />Last login attempt was at: <em>{$last_login}</em>.</p>";
}
// 登录成功后,重置用户的失败登录次数
$data = $db->prepare('UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;');
$data->bindParam(':user', $user, PDO::PARAM_STR);
$data->execute();
} else {
// 登录失败处理
sleep(rand(2, 4)); // 随机延迟2-4秒,防止暴力破解
// 给出登录失败提示
echo "<pre><br />Username and/or password incorrect.<br /><br/>
Alternatively, the account has been locked because of too many failed logins.<br />
If this is the case, <em>please try again in {$lockout_time} minutes</em>.</pre>";
// 失败次数加1
$data = $db->prepare('UPDATE users SET failed_login = (failed_login + 1) WHERE user = (:user) LIMIT 1;');
$data->bindParam(':user', $user, PDO::PARAM_STR);
$data->execute();
}
// 无论登录成功还是失败,更新最后登录时间
$data = $db->prepare('UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;');
$data->bindParam(':user', $user, PDO::PARAM_STR);
$data->execute();
}
// 生成新的Anti-CSRF令牌
generateSessionToken();
?>