在当今互联网的广袤世界中,各式交互平台层出不穷。每一个交互平台几乎都要求用户注册账号,而这些账号则成为我们在数字世界中的身份象征。账号的安全性变得至关重要。
然而,账号安全常常面临着暴力破解这一威胁。暴力破解是一种对账号安全构成严重威胁的行为。这类攻击试图通过尝试大量密码组合来非法获取账号的访问权限。这种行为不仅对个人隐私构成风险,还可能导致个人信息泄露和账号被盗用的情况发生。
验证码则是账号安全的守护者之一,它是一道关卡,旨在确保登录或重置密码等敏感操作仅供合法用户进行,从而防止未经授权的访问和暴力破解。
暴力破解是指通过尝试大量可能的密码组合,以非常快速和自动化的方式来破解密码或获取未经授权的访问权限。攻击者使用各种算法和程序,不断尝试密码,直到找到正确的密码或者绕过安全措施,进而获取对特定系统、账号或数据的访问权限。
这种攻击通常是基于“试错”的原理,攻击者不断尝试不同的组合,直到找到有效的密码或者绕过安全系统。暴力破解是一种常见的密码攻击方式,对于网络安全构成了严重的威胁,可能导致账号被盗用、信息泄露等安全问题。
暴力破解的方式多种多样,攻击者可以利用各种技术和工具来尝试破解密码。以下是一些常见的暴力破解方式:
在DVWA靶场中,每一种漏洞都有4种难度,分别为,低、中、高、办不到,随着难度的提升,验证的机制越发完善。
随意输入账号信息,查看页面报错信息,利用报错信息,分析账号是否存在。
根据报错信息提示,“用户名或密码错误”,无法判断输入账号是否正确,所以只能采用burp suite intruder模块中的集束炸弹进行遍历破解。
初步分析,该请求包有三个参数,username
、password
、login
。
username
对应:账号password
对应:密码login
对应:登录按钮所以我们要对username
、password
这两个参数进行设置变量,选择选择集束炸弹模式进行遍历攻击。
将数据包发送到Intruder模块中,在positions模块下将username
、password
添加变量。
在payloads中添加需要遍历攻击的值。变量1为账号信息,变量2为密码信息。
这里因为我们是简单测试就只是用了simple list作为payload类型进行传入变量,在现实场景中,可以根据实际情况选择其他类型进行传值,例如你这就有一个账号和密码字典 可以选择simple list 下的load 在载入你自己的字典
进行攻击后,我们先查看length长度字段,检查所有数据包中长度有变化,进行分析。
为什么要查看长度变化,因为在登录页面中,成功登录和失败登录可能返回不同的页面内容或报错信息,这些内容的长度可能是不同的。
我们就可以利用这一点,通过观察响应的长度来判断他们的猜测是否成功。如果尝试的密码或参数是正确的,服务器通常会返回一个不同长度的响应页面,这可能是登录成功的指示。相反,如果登录失败,返回的页面长度可能会与预期的不同。
在此处,发现两个数据包的长度,与其他数据包明显不一致,查看响应包发现已经登录成功
这里还有一个小技巧,使用设置(settings)中的Grep - Match 功能,可以根据关键词快速定位登录成功的数据包
例如在本关卡中,成功登录的关键词是welcome
,我们就可以在这里先启用Grep - Match
在吧关键词welcome
添加进过滤规则当中
然后重新进行遍历攻击。
此时,攻击结果中出现了一个,welcome
字段,这里就是我们刚才设置的关键字,一旦响应包中出现这个关键字,那么这个字段就会标注为1.
在本关卡中,登录功能,没有进行任何验证,这导致攻击者可以无限的尝试账号和密码,一直到找到正确账号密码为止,我们来看看源码都写了什么!
if( isset( $_GET[ 'Login' ] ) ) {
$user = $_GET[ 'username' ];
$pass = $_GET[ 'password' ];
$pass = md5( $pass );
//对传入的参数未作任何验证,
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
//这里还导致的SQL注入
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( ''
. ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
' );
//这里使用了三元运算嵌行了结果的判断,$GLOBALS["___mysql_ston"]是一个全局变量,他里面应该存储 MySQLi连接对象,用于连接数据库进行查询,在之后的在or die() 函数中,嵌套了一个三元条件运算符,如果查询结果错误则判断$GLOBALS["___mysql_ston"]是否为对象,如果不是对象,则检查 mysqli_connect_error() 是否返回了连接错误信息,并返回相应的信息。它只是一个报错逻辑,并没有验证功能。
if( $result && mysqli_num_rows( $result ) == 1 ) {
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
$html .= "Welcome to the password protected area {$user}
"; $html .= ""; //结果错误,提示错误信息! } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); //这行代码使用了三元条件运算符来关闭 MySQLi 连接对象,并返回一个布尔值表示关闭连接的成功与否。 } ?>
Username and/or password incorrect.
在源码分析时还发现了SQL注入的问题,选择我们来验证以下,使用万能密码进行测试
1‘ or 1=1#
提示账号密码错误,测试我深刻的怀疑了我的代码审计能力,是不是我审计的有问题。
于是我重新测试了了单引号‘
出现了报错,才缓过神来。
既然存在SQL页面报错,那么就一定是存在SQL注入的,最终发现,是我代码审计的时候疏忽了,如果我使用了 1’ or 1=1#,此时会遍历出表中所有数据,从而导致 $result
成为了一个二维数组,if( $result && mysqli_num_rows( $result ) == 1 )
就为flase
,最终导致无法登录。
以闭合SQL为主,又想了一条POC进行测试
admin' or '1'='1
这条语句的完全体就是
SELECT * FROM `users` WHERE user = '$user' OR '1' = '1' AND password = '$pass';
主要利用里SQL中 OR
和AND
的特性,因为AND
的优先级高于OR
,所以它会先执行 '1' = '1' AND password = '$pass'
, 那么WHERE
的条件就会变成'$user' OR Flase
此时只要$user
传入是一个真值,那么查询语句就为真,就可以绕过密码登录成功。
当然也可以利用这条SQL注入去进行更深入的攻击,不过本篇讲的是暴力破解,也就不在追究了!
在low篇章中,以及详解讲解了思路,所以中级篇章就不在详细讲解了。
直接使用Burp suite 抓包分析
发现Medium和low的请求包基本没有太大区别,在请求报文中,没法发现任何验证参数字段。直接使用low的方法进行爆破!
在爆破时候发现响应时间非常慢,可能是后端做了限制,待会分析源码的时候看看,到底是怎么回事!
在长时间的等待后,终于还是爆破出来可以登录的账号,凭感觉说,Medium和low基本没区别,只是响应包的时间做了限制,我们分析源码看看具体原因
if( isset( $_GET[ 'Login' ] ) ) {
$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)) ? "" : ""));
//使用了mysqli_real_escape_string()函数对sql注入的特殊字符进行了转义,SQL注入修复了
$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)) ? "" : ""));
//使用了mysqli_real_escape_string()函数对sql注入的特殊字符进行了转义,SQL注入修复了
$pass = md5( $pass );
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( ''
. ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
' );
//和 low一样
if( $result && mysqli_num_rows( $result ) == 1 ) {
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
$html .= "Welcome to the password protected area {$user}
"; $html .= ""; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>
Username and/or password incorrect.
从源码来看,Medium对low出现的sql注入问题,进行了修复,使用了mysqli_real_escape_string()
函数进行了特殊字符转义,响应时间缓慢的原因也找出来了,在else的代码体中使用了sleep()
函数进行了延迟处理,只要登录发生错误,就会延迟2秒在返回响应包!
老规矩了,直接上bp抓包查看请求包数据!
从请求包中发现,出现了一个user_token
字段,看样子是使用了token(令牌)机制来对登录进行了控制
知识补充:
Token(令牌)通常用于身份验证或授权过程中,是一个代表特定权限或访问权限的字符串或数据。它可以是一个加密的字符串、数字、或其他形式的标识符,用来验证用户的身份或授权用户执行特定操作。想象一下,你拿着一张门禁卡去公司,这张卡就是一个令牌。当你刷卡进入公司大楼时,门禁系统会验证这张卡的有效性,确认你有权限进入。在网络世界中,令牌就像是这张门禁卡,它可以表示你是谁、你有什么权限,或者允许你执行什么操作。
在 Web 登录中,Token 是用于身份验证和会话管理的一种机制,用于验证用户的身份和授权用户访问特定的资源或执行特定操作。主要有两种类型的 Token 在 Web 登录中广泛使用:
- Session Token(会话令牌): 当用户登录时,服务器会创建一个会话,并为该会话分配一个唯一的会话标识符(通常是一个随机生成的字符串),即 Session Token。这个 Token 通常存储在服务器端的会话存储中,比如内存或数据库中。客户端与服务器通信时,会话 Token 会被发送到客户端,通常是通过 Cookie 或 URL 参数。服务器使用这个 Token 来识别用户会话,并验证用户的身份,维护用户的登录状态。当用户注销或会话过期时,这个 Token 会被销毁。
- JSON Web Token(JWT): JWT 是一种开放标准(RFC 7519),用于在网络应用中传输信息的一种紧凑的、自包含的方式。它是一种在客户端和服务器之间传输安全信息的方式。JWT 通常由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。载荷中包含了认证信息和其他附加数据,例如用户的身份信息、权限等。JWT 通常存储在客户端(比如浏览器的 localStorage 或 sessionStorage 中),在每次请求时发送给服务器。服务器在收到 JWT 后会验证其真实性和有效性,并据此确定用户的身份和权限。
这些 Token 在 Web 登录中起到了关键的作用,用于验证用户身份、维护用户的登录状态,并授权用户访问特定的资源或执行特定的操作。不同的应用场景可能会选择不同类型的 Token 机制来实现登录和身份验证。
在本关卡中,Token 机制被用于控制用户重复登录和防止暴力破解。这种情况下,系统可能在用户每次登录时生成一个临时的 Token,而不是在成功登录后给用户分配一个长期有效的会话标识符。
那么每一次登录失败后,都要刷新页面获取新的Token来进行登录。这里就造成一个问题,我们使用BP去爆破肯定都是一直在一个页面去操作,不能动态的获取新的Token,这就导致了无法正常测试。
在burp suite中 有一个功能是Grep - Extract
这个功能是在流量中,对数据经行提取,我们可以使用这个功能从每次刷新中去提取新的token值。
但是在本关卡中使用,还有一个前提条件,就是我们要去刷新页面来获取新的token,这里就需要使用到Redirections(重定向) 的功能了。
它下面参数的含义如下:
Never: 这个选项指示 Burp 不要跟随重定向。如果设置为 “Never”,当收到重定向响应时,Burp 将不会自动跳转到新的 URL,而是会停止并显示原始请求的响应。
On-site: 这个选项告诉 Burp 仅在重定向目标与原始请求位于同一站点时才跟随重定向。如果设置为 “On-site”,Burp 将仅在目标 URL 与原始请求的 URL 具有相同的站点(即相同的主机和端口)时,才会自动跳转到新的 URL。
Only in-scope: 此选项指示 Burp 仅在目标 URL 在 Burp Suite 的范围内时才跟随重定向。如果设置为 “Only in-scope”,Burp 只有在目标 URL 包含在你当前 Burp 项目的范围内(即在目标范围内)时才会自动跳转。
Always: 这个选项告诉 Burp 无论何时都要跟随重定向。如果设置为 “Always”,无论目标 URL 在哪里,Burp 都会自动跳转到新的 URL。
Process cookies in redirections"(处理重定向中的 Cookie)选项用于控制在进行重定向时是否处理和传递 Cookie。
当这个选项被启用时,Burp Suite 会在重定向请求中处理和传递 Cookie。也就是说,当服务器返回重定向响应时,Burp Suite 会在后续的请求中携带先前收到的 Cookie,以确保在整个重定向链中保持会话状态。
如果禁用了这个选项,Burp Suite 在重定向请求中将不会处理或传递 Cookie。这意味着在重定向链中,后续请求将不会携带之前收到的 Cookie,可能导致会话状态的丢失或不一致。
为了避免麻烦我们使用,Always选项,告诉burp suite 要一直跟随重定向!
第一步还是抓包
将抓到的包扔到Intruder模块中,并定义password
和usertoken
两个变量,并选择草叉模式。
为什么要这么设置,因为都是血淋淋的教训。
焦点炸弹模式的问题: 在焦点炸弹模式中,一个变量(例如 password
)会与所有其他变量的每个值进行组合,这可能导致一个特定的 usertoken
(token 值)与大量的不同密码值一起测试,而且这种情况下重定向可能跟不上,导致 token 失效。
草叉模式的优势: 草叉模式确保了变量之间的一一对应关系。如果您的攻击需要保持 usertoken
和 password
的一致性,那么使用草叉模式是比较合适的。它会同时遍历两个变量的所有可能组合,而不会导致单个 usertoken
与大量不同的 password
值进行组合。
去掉账号的变量: 如果攻击中已经确定了账号(例如 usertoken
),并且您希望针对不同密码进行爆破测试,那么将账号作为固定值可能不会很有效。草叉模式要求所有变量都具有一一对应的关系,因此,为了有效地测试不同的密码而不是不同的账号,我们就需要去掉账号这个变量。
然后进入setting,找到grep-extract,进行流量提取的设置.
选择add进入添加界面,在点击Refetch response 进行刷新,出现响应包后,找到user_token的值,双击选中。
选中后 第一个以下图中第一个内容框内就是这个值的键,第二个框内就是这个值。提醒以下这里一定要复制这个token待会有用处!
配置好后,在规则库中应该有以下内容
在找到重定向功能,并选择Always!
此时还有一件事要做,不然就无法启动爆破遍历攻击!找到线程设置,新建一个线程规则,设置线程为1,
因为payload的 recursive grep 类型不支持多线程,必须把线程设置为1才能启动,否则将会报错!
配置如下
在进入到payloads功能下,对两个变量进行赋值!变量1 设置密码字典
变量 2要更换payload 类型,为recursive grep类型,并将刚才复制的token复制到下面文本框中。
准备工作完成后可以开始爆破了!
成功拿下,token的原理也基本都了解了,在日后的更新中,也会详解介绍token的使用方法!
if( isset( $_GET[ 'Login' ] ) ) {
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
//这里使用Request方法传入了一个user_token应该是用户提交的令牌,Session超全局变量应该是服务器端存储的令牌,还传入了一个index.php文件应该是重定向的url,是一个自定义的函数,待会详细解释。
$user = $_GET[ 'username' ];
$user = stripslashes( $user );
//与Medium不同,使用双重过滤 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)) ? "" : ""));
$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)) ? "" : ""));
$pass = md5( $pass );
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( ''
. ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
' );
if( $result && mysqli_num_rows( $result ) == 1 ) {
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
$html .= "Welcome to the password protected area {$user}
"; $html .= ""; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } generateSessionToken(); //这里也是一个自定义函数,按字面意思理解是重新生成了一个token ?>
Username and/or password incorrect.
此源码与Medium基本一致,多了token的机制,和stripslashes()的函数,还有两个自定义的函数checkToken()和generateSessionToken().
function checkToken($user_token, $session_token, $returnURL)
{
global $_DVWA;
//创建了一个全局变量 $_DVWA
if (in_array("disable_authentication", $_DVWA) && $_DVWA['disable_authentication']) {
return true;
//它检查了一个名为 $_DVWA 的全局变量中是否存在名为 disable_authentication 的键,并且它的值为真(即 $_DVWA['disable_authentication'] 是 true)时,直接返回 true。这个检查可能是用于在某些情况下禁用 CSRF 保护。
}
if ($user_token !== $session_token || !isset($session_token)) {
//接下来,函数检查用户提交的令牌 $user_token 是否和服务器端存储的令牌 $session_token 不同或者 $session_token 是否未设置。
dvwaMessagePush('CSRF token is incorrect');
//将一条消息推送到消息队列中,消息内容为 'CSRF token is incorrect',提示 CSRF 令牌不正确。
dvwaRedirect($returnURL);
//重定向到指定的 $returnURL。
}
}
function generateSessionToken()
{
if (isset($_SESSION['session_token'])) {
destroySessionToken();
//首先检查是否已经存在名为 'session_token' 的会话变量。如果存在,它会调用 destroySessionToken() 函数来销毁现有的会话令牌。
}
$_SESSION['session_token'] = md5(uniqid());
//使用 md5(uniqid()) 生成一个新的会话令牌,并将其存储到 $_SESSION['session_token'] 中。
}
自定义函数中的代码逻辑~!
function dvwaMessagePush($pMessage)
{
$dvwaSession = &dvwaSessionGrab();
//似乎是用于获取 DVWA 的会话对象。
if (!isset($dvwaSession['messages'])) {
//检查会话中是否存在名为 'messages' 的键,如果不存在,就创建一个空数组来存储消息。
$dvwaSession['messages'] = array();
//将传入的 $pMessage 消息添加到 'messages' 数组中。
}
$dvwaSession['messages'][] = $pMessage;
}
function &dvwaSessionGrab()
{
if (!isset($_SESSION['dvwa'])) {
$_SESSION['dvwa'] = array();
//检查是否存在名为 'dvwa' 的会话变量。如果不存在,它会将一个空数组赋值给
}
return $_SESSION['dvwa'];
//返回 $_SESSION['dvwa'],这意味着它返回了 'dvwa' 这个会话变量的引用。
}
function dvwaRedirect($pLocation)
{
session_commit();
//调用 session_commit() 来提交会话数据。
header("Location: {$pLocation}");
//使用 header() 函数将页面重定向到指定的 $pLocation 地址。
exit;
//调用 exit 以确保在重定向后立即终止脚本的执行。
}
这一系列的代码看起来更像是防止CSRF攻击的代码,但是由于每次刷新页面都会生成一个新的token,他也能很好的防御暴力破解.
从字面意思来看,此关卡应该的安全系数应该是非常高,我也想见识见识!废话不多说直接抓包。
从抓包的情况来看,使用POST请求方法,使用High关卡的token机制来控制登录。猜测源码应该是High的升级版。现在我们按照High的思路先进行一般爆破尝试。
发现了一个新的报错信息,’Alternative, the account has been locked because of too many failed logins.
If this is the case, please try again in 15 minutes.‘(由于登录失败次数过多,帐户已被锁定。如果是这种情况,请在 15 分钟后重试。)
根据报错信息推测,系统可能引入了一种安全机制来防止暴力破解登录。当登录尝试失败次数超过一定阈值时,系统会自动锁定账户一段时间,防止进一步的登录尝试。
这种机制可以有效防止暴力破解攻击,因为攻击者无法在短时间内连续尝试多次登录,从而降低了攻击的成功率。
if( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {
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 );
//以上代码与High逻辑一致。
$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;' );
//使用 PDO 对象的 prepare() 方法准备了一个 SQL 查询语句,查询了名为 users 的表中特定用户的失败登录次数和最后登录时间。(:user) 是一个占位符,将在后续的绑定中被替换为实际的用户。
$data->bindParam( ':user', $user, PDO::PARAM_STR );
// 将查询中的 (:user) 占位符与变量 $user 绑定在一起。这个操作使用了 PDO 的 bindParam() 方法,将 :user 绑定到 $user 变量上,并指定了参数的数据类型为字符串类型(PDO::PARAM_STR)。
$data->execute();
//这行代码执行了之前准备的 SQL 查询语句,从数据库中获取用户的失败登录次数和最后登录时间。
$row = $data->fetch();
//这行代码使用 fetch() 方法从执行的查询结果中获取一行数据,并将其存储在 $row 变量中。这个查询应该返回的是指定用户的失败登录次数和最后登录时间的记录。
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 .= "Welcome to the password protected area {$user}
";
$html .= "
{$avatar}\" />";
// 如果验证成功且账户未被锁定
if( $failed_login >= $total_failed_login ) {
$html .= "Warning: Someone might of been brute forcing your account.
";
$html .= "Number of login attempts: {$failed_login}.
Last login attempt was at: {$last_login}.
";
}
$data = $db->prepare( 'UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
// 如果失败登录次数超过阈值,显示警告消息,并重置失败登录次数为 0
} else {
sleep( rand( 2, 4 ) );
// 使用随机函数rand()定义延迟时间
$html .= "
Username and/or password incorrect.
Alternative, the account has been locked because of too many failed logins.
If this is the case, please try again in {$lockout_time} minutes.
";
$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();
}
generateSessionToken();
?>
由此源码可以看出,想要爆破破解几乎是不可以能的,虽然安全性增加了,但是可用性也相对降低了。
验证码最初是一种用于确认用户是人类而不是计算机程序的技术。它通常表现为一段包含数字、字母、图片、文字或是数学问题等形式的测试,需要用户在注册、登录或进行特定操作时进行填写或回答。
验证码可以采用多种类型和形式,用于确认用户或应用程序的身份,通常用于防止恶意机器人或自动化程序对系统进行攻击。以下是一些常见的验证码类型:
这些验证码类型的共同特点之一是它们需要在后端或前端生成,并通过用户界面展示给用户,以进行验证。所以我觉得验证码按这种特性来分,就分为前端验证和后端验证:
没什么好说的,第一件事还是抓包分析!
在请求包中,未发现任何验证字段,可以直接进行爆破攻击!
使用焦束炸弹模式,进行爆破!
轻轻松松爆破成功!
$link=connect();
if(isset($_POST['submit']) && $_POST['username'] && $_POST['password']){
// 检查是否已经提交了表单数据,并且提交的数据中包含用户名和密码
$username = $_POST['username'];
$password = $_POST['password'];
// 获取通过 POST 请求传递的用户名和密码
$sql = "select * from users where username=? and password=md5(?)";
// 创建 SQL 查询语句,查找数据库中与给定用户名和 MD5 加密后密码匹配的用户记录,? 是占位符,用于表示稍后将要绑定的变量或参数的位置。在执行查询之前,这些占位符将被实际的值替换。
$line_pre = $link->prepare($sql);
// prepare() 是预处理语句的函数之一,用于准备执行 SQL 查询或命令。预处理语句在执行 SQL 查询之前将查询与参数分离,允许多次执行相同的查询,只需改变参数而不是整个查询。
$line_pre->bind_param('ss',$username,$password);
// 使用预处理语句绑定用户名和经过 MD5 加密后的密码到查询中
if($line_pre->execute()){
$line_pre->store_result();
if($line_pre->num_rows>0){
// 存在问题因为登录只能有一个用户,所以应该使用 == 1,>0可能导致万能密码
$html.= ' login success
';
} else{
// 如果查询结果为空,即未查询到匹配的用户名和密码
$html.= ' username or password is not exists~
';
// 显示用户名或密码不存在的消息
}
} else{
// 如果执行查询时发生错误
$html.= '执行错误:'
.$line_pre->errno.'错误信息:'.$line_pre->error.'';
// 显示执行错误的消息,包括错误号和错误信息
}
}
这串代码初步查看,感觉应该是存在SQL注入的问题,因为在传入参数的时候未做参数过滤、验证。
实际上,该源码通过使用了使用了预处理语句和参数绑定的方式的方式,巧妙的避免了SQl注入的问题,因此,尽管在代码中没有明显的参数过滤或验证,但使用了预处理语句的 bind_param 方法将变量安全地插入到 SQL 查询中,有效地防止了恶意用户输入可能造成的 SQL 注入问题。
SQL代码详解:
$link
这个变量接收一个数据库连接的自定义函数connect()
,在这里函数体内使用了@mysqli-connet()
函数作为连接数据库,连接后会返回一个数据库对象。然后在后面的代码里,使用
$line_pre
变量接收了$link
数据库对象中的一个prepare($sql)
的方法,$sql
是定义的一条SQL查询语句,使用了占位符?
来表示待绑定的参数位置。,此时$line_pre
接收的是一个准备好的 SQL 查询的数据库对象。
mysqli->prepare()
:这个函数的作用是预处理 SQL 查询语句,并返回一个准备好的语句对象(stmt对象),以便稍后执行。预处理过程使得数据库能够对查询进行解析、编译和优化,同时还能提供更高的安全性,可以有效预防 SQL 注入攻击。然后使用
$line_pre->bind_param(‘ss’,$username,$passowrd
)将变量$username
和$password
分别绑定到查询语句中的两个占位符上,这两个占位符分别用's'
表示字符串类型。
stmt->bind_param()
是用于将变量绑定到预处理语句中的占位符的方法。它允许你将变量的值与查询中的参数位置关联起来,以确保安全地将值插入到 SQL 查询中。这个方法可以传入多个参数原型如下:
$stmt->bind_param("types", $param1, $param2, ...);
bind_param()
方法的参数包含两部分信息:参数类型和要绑定的变量。它的基本语法是:
"types"
:表示要绑定的参数类型。这个字符串中的每个字符都代表一个参数的类型。
"s"
表示字符串类型。"i"
表示整数类型。"d"
表示双精度浮点数类型。"b"
表示存储二进制数据的类型。- 这个字符串中的每个字符都对应一个后面要绑定的变量。
$param1, $param2, ...
:要绑定到预处理语句中占位符位置的变量。
- 这些变量的类型和数量应该与
"types"
参数中指定的一致。- 本关卡的源码中,
’ss‘,’$username’,’$password’
,就代表有两个字符串类型的参数。
$line_pre->execute()
这个函数,主要用于执行smst
对象中的语句,如果执行成功返回True
,负责返回Flase
,这里使用if语句,就是为了查看SQL语句是否成功执行。
$line_pre->store_result()
,这里是使用smst->store_result()
将查询的结果存储在客户端的缓存中。
$line_pre->fetch()
这个函数,当将smst->store_result()
结果缓存在客户端后,fetch()
方法可以用来逐行获取查询结果的数据。它会返回当前行的数据,并将指针移动到结果集中的下一行。
$line_pre->num_rows
返回一个整数,表示查询结果集中的行数,在本关源码中,它是用if语句来判断是否有返回结果, 大于0,则代表登录成功,但是这里给感觉存在一个漏洞,
确实,你的想法是非常正确的。在处理登录时,通常应该只有一个用户与所提供的凭据匹配。因此,判断查询结果的行数是否等于 1 才更为严谨,而不是大于 0。如果行数大于 1,或者仅仅判断大于 0,可能会导致安全性问题,因为可以利用 SQL 注入来绕过认证机制,从而获取多个或全部用户的信息。
要确保安全,建议在验证登录时使用
num_rows == 1
来判断查询结果是否只有一个匹配。这样做可以更好地避免 SQL 注入攻击,保障系统的安全性。
这里是使用参数占位来控制SQL注入的问题,那么它的原理是什么呢?
首先先看 $sql = "select * from users where username=? and password=md5(?)";
这条语句,发现?
问号参数是没有引号的,在使用$line_pre->bind_param(‘ss’,$username,$passowrd
)来传递参数时,它会根据第一个参数来判断参数类型,在将其作为参数传递到 SQL 查询之前,预处理语句会自动对这些参数进行处理,并确保在需要的情况下自动添加引号,转义特殊字符,或以其他安全的方式处理。
针对后端验证码爆破,首先要测试验证码是否会过期,如果确定后端验证码会过期,就要采取每次爆破遍历时,去重新获取后端验证码,类似DVWA中绕过token机制。所以要将他放到Burp suite的重放模块,重放测试。查看返回页面的结果。在此处我故意将验证码输入错误,发现会返回验证码错误。
再次吧验证码修改正确的测试,发现放回用户名或密码错误,此处间接证明了验证码没有设置过期时间,一直都在重复使用。那么就可以直接开始爆破遍历了。
因为验证码没有过期时间,此时只要一直使用该验证码,就可以一直爆破遍历,直到成功位置。
$link=connect();
$html="";
if(isset($_POST['submit'])) {
if (empty($_POST['username'])) {
$html .= "用户名不能为空
";
} else {
if (empty($_POST['password'])) {
$html .= "密码不能为空
";
} else {
if (empty($_POST['vcode'])) {
$html .= "验证码不能为空哦!
";
} else {
// 验证验证码是否正确
if (strtolower($_POST['vcode']) != strtolower($_SESSION['vcode'])) {
$html .= "验证码输入错误哦!
";
//应该在验证完成后,销毁该$_SESSION['vcode']
}else{
$username = $_POST['username'];
$password = $_POST['password'];
$vcode = $_POST['vcode'];
$sql = "select * from users where username=? and password=md5(?)";
$line_pre = $link->prepare($sql);
$line_pre->bind_param('ss',$username,$password);
if($line_pre->execute()){
$line_pre->store_result();
//虽然前面做了为空判断,但最后,却没有验证验证码!!!
if($line_pre->num_rows()==1){
//此处已经做了修复 查询结果行==1
$html.=' login success
';
}else{
$html.= ' username or password is not exists~
';
}
}else{
$html.= '执行错误:'
.$line_pre->errno.'错误信息:'.$line_pre->error.'';
}
}
}
}
}
}
查看源码后总结如下:
$line_pre->num_rows
进行的逻辑修复前端验证不适用像登录界面这种敏感页面,它只适用于捕获用户输入的错误或无效的响应,并在用户提交表单之前提供即时的反馈的页面。为什么这么说呢?
在前端开发中,HTML
、CSS
和JavaScript
是不可或缺的三剑客。HTML用于构建页面结构,CSS负责页面样式的设计,JavaScript则定义页面的交互和逻辑。前端的核心工作是与用户进行交互,而这三者都需要发送到用户端,以便客户端渲染和呈现页面。
问题就出现在这里,例如我在浏览器中直接禁用javaScript
脚本,那么验证码还能执行验证么?,或者我直接修改了验证码脚本的结构,让它不再提供验证,作为管理员的你是否又能知道,我是否真的通过了验证呢?
因此,前端验证应该被视为一种辅助手段,用于提高用户体验和即时反馈,但不能作为唯一的安全屏障。
因为是前端验证,所以它不会产生流量,使用bp也抓不到验证码的包,所以在这里我们先手动验证我的思路!先随意输入验证码,会弹出验证码输入错误的提示框!
使用页面检查工具,定位到form表单,发现内部有一个onsubmit提交属性,提交后会返回一个validate()
的验证函数
在调试器中,我找到了这个函数的定义,它是一个判断验证码是否正确的函数,那么此时我把这个给删除会发生什么事情呢?
无验证码,也可以正常登录了!
删除了前端验证,我们在使用burp suite进行重放验证!这里未使用验证码,输入随意账号密码后,只爆出了用户名密码错误,并没有在报出验证码为空或错误!
在浏览器的设置中,禁用JavaScript脚本的功能,但是这个功能在一般情况下都不建议使用,因为在一个网页中,不仅仅只有验证码这么一个逻辑,还有非常多的页面操作逻辑,如果暴力的禁用掉JavaScript就会导致,整个网站的脚本都无法正常使用。这就得不偿失了。
在burp suite中也有类似的功能,不过它可以更灵活的使用,它在proxy中setting中~
这些选择对我们分析web页面有着巨大好处,由于本次是在进行前端验证的绕过,所以我就选择第4个移除表单验证,来测试!
可以发现通过burp suite 处理相应包,其中onsubmit
事件属性已经不见了。明白了前端验证的原理后,接下来的密码爆破就不在演示了,和之前一模一样!
后端PHP代码就不用在分析了,基本和基于表单的源码一直,我们着重看看javascript的验证代码:
<script language="javascript" type="text/javascript">
var code; //在全局 定义验证码
function createCode() {
code = "";
var codeLength = 5;//验证码的长度
var checkCode = document.getElementById("checkCode");
var selectChar = new Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9,'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z');//所有候选组成验证码的字符,当然也可以用中文的
for (var i = 0; i < codeLength; i++) {
var charIndex = Math.floor(Math.random() * 36);
code += selectChar[charIndex];
}
//alert(code);
if (checkCode) {
checkCode.className = "code";
checkCode.value = code;
}
}
function validate() {
var inputCode = document.querySelector('#bf_client .vcode').value;
if (inputCode.length <= 0) {
alert("请输入验证码!");
return false;
} else if (inputCode != code) {
alert("验证码输入错误!");
createCode();//刷新验证码
return false;
}
else {
return true;
}
}
createCode();
</script>
总共就写了两个函数:
createCode()
用于生成验证码, validate()
则用于验证码的验证工作!逻辑都非常简单。
这一关与DVWA的token绕过基本一致大致流程如下:
抓包
设置过滤提取token
把线程改为1
使用草叉模式设置变量1为密码字典、变量2为查找递归类型
就可以爆破成功!
账号安全在数字时代确实是至关重要的,而弱密码是安全风险的主要来源之一。对此,防范暴力破解是至关重要的一步。使用强大的密码策略、多因素身份验证和账户锁定功能等是防范暴力破解的有效手段。
同时,确保设备的安全性与可用性之间取得平衡也是关键。采用token与密码错误次数结合的方式可以在一定程度上保护账号免受暴力破解,但这可能会牺牲一定的可用性。因此,数据敏感等级的分析和安全策略的制定非常重要。根据数据的敏感等级,可以采取不同的保护措施,确保安全性的同时最大程度地保持系统的可用性。
对于高敏感等级的数据,可以采用更加严格的安全措施,例如限制登录尝试次数、增强密码复杂度要求、实施更严格的身份验证等。而对于低敏感等级的数据,则可以采用较为灵活和轻松的安全措施,以保证系统的可用性和用户体验。