DVWA靶场通关笔记-暴力破解(Impossible级别)
目录
一、查看源码
二、功能分析
三、SQL注入分析
1、使用PDO预处理语句和参数绑定
2、mysqli_real_escape_string转义
3、stripslashes去除反斜杠
四、暴力破解分析
1、token防止暴力破解机制
2、登录失败随机延迟机制
3、登陆失败报错信息相同
4、登陆失败的账户锁定机制
本系列为通过《DVWA靶场通关笔记》的暴力破解关卡(low,medium,high,impossible共4关)渗透集合,通过对相应关卡源码的代码审计找到讲解渗透原理并进行渗透实践,本文为暴力破解impossible关卡的原理分析部分,讲解相对于low、medium和high级别,为何对其进行渗透测试是Impossible的。
一、查看源码
进入DVWA靶场源目录,找到impossible.php源码,分析其为何能让这一关卡名为不可能实现暴力破解。
打开impossible.php文件,对其进行代码审计,详细注释的impossible.php源码如下所示。
<?php// 检查是否通过 POST 方法提交了名为 'Login' 的表单数据,并且用户名和密码字段均已设置
if( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {// 检查反跨站请求伪造(CSRF)令牌// $_REQUEST['user_token'] 是从请求中获取的用户令牌// $_SESSION['session_token'] 是存储在会话中的令牌// 'index.php' 是如果令牌验证失败将跳转的页面checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// 从 POST 请求中获取用户输入的用户名$user = $_POST[ 'username' ];// 去除用户名中的反斜杠(如果有的话)$user = stripslashes( $user );// 使用 mysqli_real_escape_string 函数对用户名进行转义处理,防止 SQL 注入$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)) ? "" : ""));// 从 POST 请求中获取用户输入的密码$pass = $_POST[ 'password' ];// 去除密码中的反斜杠(如果有的话)$pass = stripslashes( $pass );// 使用 mysqli_real_escape_string 函数对密码进行转义处理,防止 SQL 注入$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)) ? "" : ""));// 使用 md5 函数对密码进行加密$pass = md5( $pass );// 设置默认值// 允许的最大失败登录次数$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' ];// 登录成功,显示欢迎信息和用户头像$html .= "<p>Welcome to the password protected area <em>{$user}</em></p>";$html .= "<img src=\"{$avatar}\" />";// 如果账户之前被锁定过,给出警告信息if( $failed_login >= $total_failed_login ) {$html .= "<p><em>Warning</em>: Someone might of been brute forcing your account.</p>";$html .= "<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 {// 登录失败,脚本暂停 2 到 4 秒之间的随机时间sleep( rand( 2, 4 ) );// 给出登录失败的反馈信息$html .= "<pre><br />Username and/or password incorrect.<br /><br/>Alternative, 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>";// 更新失败登录次数$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();
}// 生成反跨站请求伪造(CSRF)令牌
generateSessionToken();?>
二、功能分析
此 PHP 代码实现了一个具备反 CSRF 保护、防暴力破解机制的用户登录验证系统。当用户通过 POST 方法提交登录表单时,代码会先检查 CSRF 令牌的有效性,接着对用户名和密码进行处理,包括去除反斜杠和转义处理,再对密码进行 MD5 加密。之后会从数据库中查询用户的失败登录次数和最后登录时间,判断账户是否被锁定。若账户未锁定且用户名和密码匹配,则登录成功,显示欢迎信息和用户头像,同时重置失败登录次数;若登录失败,会暂停一段时间并给出反馈信息,同时更新失败登录次数。无论登录结果如何,都会更新用户的最后登录时间。最后,会生成一个新的反 CSRF 令牌。
三、SQL注入分析
代码中使用了如下三种方法进行防御,具体如下所示。
- 使用PDO预处理语句和参数绑定
- 对用户名和密码输入进行了mysqli_real_escape_string转义
- 使用了stripslashes去除反斜杠
1、使用PDO预处理语句和参数绑定
PDO预处理语句是SQL注入防护的黄金标准。通过参数绑定,用户输入的数据不会被解释为SQL代码的一部分,而是作为纯数据处理。这种方式完全隔离了代码和数据,即使输入中包含SQL特殊字符(如单引号、分号等),也不会改变SQL语句的结构。这是最有效的SQL注入防护措施,从根本上消除了SQL注入风险的可能性。
$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();
2、mysqli_real_escape_string转义
mysqli_real_escape_string
对特殊字符进行转义,特别是在字符串上下文中。它会转义单引号(')、双引号(")、反斜杠()和NULL字符等,防止这些字符破坏SQL语句结构。虽然这在PDO预处理的基础上是冗余的,但提供了额外的防御层。这种深度防御策略确保了即使预处理机制出现问题,转义功能仍然能提供保护。
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user ) : "");
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : "");
3、stripslashes去除反斜杠
stripslashes
函数去除输入中的反斜杠,主要用于处理Magic Quotes功能开启时自动添加的转义字符。虽然现代PHP版本默认关闭Magic Quotes,但这个处理确保了向后兼容性。它防止了双重转义问题,确保数据在进入数据库前保持正确的格式。这是防御深度策略的一部分,处理各种可能的输入场景。
$user = stripslashes( $user );
$pass = stripslashes( $pass );
综上,代码在处理用户输入时,使用了stripslashes和mysqli_real_escape_string对用户名和密码进行处理,并且在后续的数据库查询中使用了 PDO 预处理语句。PDO 预处理语句本身能够有效防止 SQL 注入,因为它将 SQL 语句和用户输入分开处理,数据库会自动对输入进行正确的转义。但stripslashes的使用可能会破坏mysqli_real_escape_string的转义效果,不过由于后续使用了 PDO 预处理语句,整体上 SQL 注入的风险相对较低。
四、暴力破解分析
代码中使用了如下三种方法进行防御暴力破解,具体如下所示。
- 基于token防暴力破解验证机制
- 失败登录后有随机延迟(2-4秒)
- 所有登录失败都打印同一错误信息,不利于攻击者枚举有效用户名
- 登陆失败后的账户锁定机制
1、token防止暴力破解机制
CSRF令牌验证有效防止自动化暴力破解。攻击者必须首先获取有效的user_token才能提交登录请求,这增加了攻击复杂度。每个会话的令牌唯一且有时效性,阻止了重放攻击和批量自动化请求。Token机制确保了每个登录请求都是来自合法的用户会话,显著提高了暴力破解的门槛,迫使攻击者需要先破解CSRF保护才能进行密码猜测。
// 检查反跨站请求伪造(CSRF)令牌
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
2、登录失败随机延迟机制
随机2-4秒延迟极大降低了暴力破解的速度。传统攻击每秒可尝试数十次,现在降至每分钟仅15-30次。对于万级密码字典,攻击时间从几分钟延长到数小时。随机性防止攻击者通过固定时间模式优化攻击节奏,增加了不确定性。这种时间成本策略有效消耗攻击者资源,使得大规模暴力破解在经济和时间上变得不可行,显著提升防御效果。
sleep( rand( 2, 4 ) );
3、登陆失败报错信息相同
统一错误信息有效防止用户名枚举攻击。攻击者无法通过错误消息区分"用户名不存在"和"密码错误",必须同时猜测正确的用户名和密码组合。
$html .= "<pre><br />Username and/or password incorrect.<br /><br/>Alternative, 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>";
这使得攻击复杂度从O(n+m)增加到O(n×m),显著扩大搜索空间。虽然当前实现仍透露了锁定信息,但基本防止了通过错误信息差异进行用户枚举,迫使攻击者采用更耗时耗力的组合攻击方式。如下所示,如果登录错误,报错信息均如下所示,并不会出现只是用户名错误,密码错误,使攻击者不清楚到底是输入参数中哪个信息是不正确的。
4、登陆失败的账户锁定机制
记录了失败登录次数和最后登录时间,有账户锁定机制(3次失败后锁定15分钟=15*60秒)。
if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) ) {$last_login = strtotime( $row[ 'last_login' ] );$timeout = $last_login + ($lockout_time * 60);if( $timenow < $timeout ) {$account_locked = true;}
}
账户锁定机制针对性地保护每个用户账户。在3次失败登录后锁定15分钟,有效阻止针对特定用户的持续暴力破解。时间基于最后登录时间计算,确保锁定期的准确性。这种机制保护了用户账户免受定向攻击,迫使攻击者要么等待锁定解除,要么转向其他目标,分散了攻击压力。结合全局延迟和令牌验证,形成了多层次的有效防护体系。
综上,代码采取了多种措施来防范暴力破解。包括token防暴力破解,设置了最大失败登录次数限制(3次),当失败次数达到上限时,账户会被锁定一段时间(3次失败后锁定15分钟);登录失败时,脚本会暂停 2 到 4 秒之间的随机时间,增加每次尝试的时间成本。但是本关卡仍然存在一些小问题:
- 锁定是基于用户名而非IP,允许攻击者针对不同用户尝试
- 锁定时间较短(仅15分钟)
- 随机延迟不够长(2-4秒不足以阻止自动化攻击)