1. DVWA_Brute Force

1. DVWA_Brute Force

 

 

1. low级别

方法思路:

服务器只是验证了参数Login是否被设置,没有任何的防爆破机制,且对参数username、password没有做任何过滤,存在明显的sql注入漏洞。可采用暴破或sql注入的方式绕过登录。

代码审计(核心部分):

<?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>' );
   // z = x or y型,若x为真,则执行x,否则执行y。

if($result && mysqli_num_rows( $result )==1) {
// Get users details
$row    = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"]; // 特别注意,sql注入时我们仅能返回的一条用户信息,否则这个位置会报错。
// Login successful
$html .= "<p>Welcome to the password protected area {$user}</p>";
$html .= "<img src="{$avatar}" />";
}
else {
// Login failed
$html .= "<pre><br />Username and/or password incorrect.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
   
/*   关于$GLOBALS["___mysqli_ston"]:
MySQLConverter设置该全局变量为您的数据库连接对象。如果MySQLConverter发现系统的数据库连接请求mysql_connect(),它将mysqli_connect函数的执行结果包含到$GLOBALS[“___mysqli_ston”]中,如下所示:
$GLOBALS[“___mysqli_ston”] = mysqli_connect($hostname,$username,$pwd);
*/

 

1.1 方法一:SQL注入-万能密码

初步思路:

见到这样无验证码的登录框,条件反射该想到暴破和sql注入。先尝试下SQL注入,随手用户名框输入单引号',发现有如下报错:

可发现是单引号字符型注入的特征,但疑惑的是除了单引号我在用户名框并没有输入任何其他内容,为啥多了串d41d8cd98f00b204e9800998ecf8427e,特征疑似MD5哈希值,解密后发现代表null,空字符串。故又一步印证源码里是单引号闭合,因为闭合后无任何内容,故返回一个代表NULL的MD5值,于是就有了注入点。

payload:

--------------  1 -----------------
用户名:' or '1'='1
-----------------------------------
注入无效。
通过对源码分析,我们在用户名框注入后原句中WHERE user = '' or '1'='1' AND password = '',相当于WHERE 0 or 1 and 0,也就是WHERE 0,自然不能成功登录。这里要说明两点:
1.由于PHP内嵌的SQL语句里WHERE子句用了AND运算符,因为AND运算优先级比OR大,很多情况下会导致逻辑运算后的实际结果与我们预期的不同
2.再者,WHERE子句里条件未赋值,默认就为NULL,而NULL作逻辑判断是默认是False。
这样的话可能会想通过密码框注入,就能解决这个优先级造成的问题。但也失败了,查看源码发现后端获取密码后做了一个MD5加密的处理,并不是直接明文存储,故我们注入的语句也随之变为MD5值而失效。

--------------- 2 -------------------
用户名:' or 1=1; #
-------------------------------------
注入无效。
由上一个分析可知,注入的点只能在用户名框处,随即想到用注释符来直接把后面的SQL语句注释掉,暴力的解决AND与OR运算符优先级的问题,但构造的注入语句' or 1=1; # 依然无效,查看源码后发现,虽然刚开始顺利通过if语句登录,但在接下来的提取用户图片等操作中,由于我们返回的是所有用户的信息,而源码中是仅从一个用户的返回信息中提取图片路径,这必然会导致错误,故转入else分支,登录失败。

--------------- 3 ------------------
用户名:' or 1=1 limit 0,1; #    
------------------------------------
注入有效。
由上一个分析可知,我们虽成功进入if语句,但在后续代码执行过程中因返回了全部的用户信息触发了错误,导致转入执行else语句而登陆失败。故可在构造的注入语句中加入limit来限制返回结果数,便可登录成功。

--------------- 4 ------------------
用户名:admin' or '1'='1          
------------------------------------
注入有效。
因为有了正确的用户名故不再受原SQL语句中的AND的优先级的影响,故我们在用户名框注入后原句中WHERE user = 'admin' or '1'='1' AND password = '',相当于WHERE 1 or 1 and 0,也就是WHERE 1,便可成功登录。

---------------- 5 ----------------
用户名:' or '1'='1
密码:letmein
-----------------------------------
注入有效。
因为有了正确的密码,使得我们按原句AND优先运算也能得到预期的结果,即WHERE user = '' or '1'='1' AND password = '0d107d09f5bbe40cade3de5c71e9e9b7',相当于WHERE 0 or 1 and 1。但要注意,这种方法存在失败的可能,因为若是对方数据库中有多个用户是用该密码,则我们返回的值就是多个,又回到了第二个分析上,且这次还无法limit限制返回个数,因为密码框的输入后端均用MD5加密。

 

1.2 方法二:暴破

由于登录界面无验证码,服务器仅通过if(isset($_GET['Login']))验证是否设置了Login参数来接收和处理用户登录请求。

故burpsuite代理 —> 拦截包 —> 爆破模块一条龙服务,成功暴破出正确的用户名密码。

 

 

 

2. medium级别

方法思路:

相比Low级别的代码,Medium级别的代码主要增加了mysql_real_escape_string()函数,这个函数会对字符串中的特殊符号进行转义,会被转移的字符有NULL(ASCII 0), , , \, ', " 和 Control-Z,基本上能够抵御sql注入攻击,但不是绝对的,因为MySQL5.5.37以下版本(高版本未知,并未测试)如果设置编码为GBK,可能会被宽字节注入进而绕过mysql_real_escape_string()函数;同时,$pass做了MD5校验,杜绝了通过参数password进行sql注入的可能性。

但是!!!依然没有加入有效的防爆破机制,虽然设置了sleep(2),登录失败时服务端会延迟两秒才继续执行当前脚本,但实在算不上一种有效防御方式,可忽略,同low级别一样暴破完事。

代码审计(核心部分):

<?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)) ? "" : ""));
/*
mysqli_real_escape_string()函数会转义字符串中的特殊字符,实现用户输入进行过滤,会被转义的特殊字符有NULL(ASCII 0), , , \, ', " 和 Control-Z。
*/
   
   // 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);
}
?>

 

 

3. high级别

方法思路:

medium就加入了mysql_real_escape_string()函数防止sql注入,high级别一定更加巩固,故不考虑SQL注入。

挂Bp后截包分析,可以看到参数user_token,说明服务器上存在Token机制。repeater进一步分析该Token机制,即可发现,每次提交请求后,不管是否成功登录,服务器返回的HTML主页里都会携带一个新的随机生成的Token,供客户下一次提交请求时携带使用。后端处理的脚本里会先验证客户端请求包里的Token值与本地session里存储的Token值是否一致,确定一致后才会执行后续的登录验证等操作,若不一致则采取其他措施,如此处网站设计者让服务器返回一个302重定向包,让我们重定向回到登录框页面。

这种机制本来是用于防止CSRF攻击的,但其实对我们的暴破也间接增加了难度,因为需要我们在暴破时,让每次发送的请求包,都得携带着上一次请求服务器返回的包里新生成的那个Token。

这种情况下,不再适用Bp的常规暴破手段(当然看网上也有人用Bp提供的py脚本模块进行暴破),为了锻炼自己写脚本的能力,所以决定自己写个py脚本进行暴破。于是有了下面这个简陋的暴破脚本:

运行脚本,虽然线程少,暴破速度慢了点,但好歹还是成功爆破出用户名和密码:admin,password。

 

代码审计(核心部分):

# high.php的完整代码

<?php

if( isset( $_GET[ 'Login' ] ) ) {
   // Check Anti-CSRF token
   checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
/*
High级别的代码加入了Token机制,可以抵御CSRF攻击,同时也增加了爆破的难度;
登录验证时提交了四个参数:username、password、Login以及user_token。
每次服务器返回的登陆页面中都会包含一个随机的user_token的值,用户每次登录时都要将user_token一起提交。
*/
   
   // Sanitise username input
   $user = $_GET[ 'username' ];
   $user = stripslashes( $user );  
   // stripslashes()函数删除字符串中的反斜杠,用于过滤用户输入中的反斜杠。若有两个连续的反斜线,则只去掉一个,进一步抵御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)) ? "" : ""));
// 同medium级别一样,使用mysqli_real_escape_string()函数转义用户输入的特殊字符。
   
   // 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();     // 每次服务器返回的登陆页面中都会包含一个随机的token。
?>
# dvwaPage.inc.php中定义的几个需关注的函数

function generateSessionToken() {  # Generate a brand new (CSRF) token
if( isset( $_SESSION[ 'session_token' ] ) ) {
destroySessionToken();
}
   // 删除上一次用户请求生成的Token,并在接下来重新创建一个。
   
$_SESSION[ 'session_token' ] = md5( uniqid() );
   // 生成一个随机Token并储存在服务端本地Session中。
   // Token实现方式:uniqid() 函数基于以微秒计的当前时间,生成一个随机且唯一的ID。
}

function tokenField() {  # Return a field for the (CSRF) token
return "<input type='hidden' name='user_token' value='{$_SESSION[ 'session_token' ]}' />";
}
// 该函数用于将我们每次随机生成的Token插入index主页返回给客户端,这样每次服务器返回的登陆页面中才会包含一个随机的token。

function checkToken( $user_token, $session_token, $returnURL ) {  # Validate the given (CSRF) token
if( $user_token !== $session_token || !isset( $session_token ) ) {
dvwaMessagePush( 'CSRF token is incorrect' );
dvwaRedirect( $returnURL );
}
}
// 该函数用于核查用户提交的Token(即$user_token)是否和我们服务器本地Session里存储的Token(即$session_token)一致。

 

 

4. impossible级别

安全设计分析:

Impossible级别被视为不存在此类漏洞的安全代码。

测试后发现,当服务器检测到某账户频繁的登录失败后,系统会将该账户锁定,暴破也就无法继续,这是一种比较可靠的防暴破机制。同时,后端PHP脚本采用了更加安全的PDO机制(PDOstatement对象)防御sql注入。

代码审计后,将其安全设计流程简要总结如下图:

代码审计(核心部分):

<?php

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

// Sanitise username input
$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)) ? "" : ""));

// Sanitise password input
$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 );

// Default values
$total_failed_login = 3;
$lockout_time       = 15;
$account_locked     = false;


   // 加入了PDO机制,通过使用PDO预处理对象来防范SQL注入攻击。
   // 对于用户每次提交的登录,首先查询该用户提交的账号总共进行登录尝试(登录失败)的次数。
$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();

// Check to see if the user has been locked out.
   // 当用户尝试登录次数超过限定的次数,判断是否到了解封时间,并可以继续下一次有效的登录提交
if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) ) {
//$html .= "<pre><br />This account has been locked due to too many incorrect logins.</pre>";

// Calculate when the user would be allowed to login again
       $timenow    = strtotime( "now" );
       $last_login = $row[ 'last_login' ];    
$last_login = strtotime( $last_login );   // 将日期转化为unix时间戳
$timeout    = strtotime( "+ {$lockout_time} minutes",$last_login );  // 用户账户解封时间        
       /*
       注意:此处源码不对,不能正常执行返回出$timeout的时间戳
       - 原句源码:$timeout   = strtotime( "{$last_login} + {$lockout_time} minutes" );
       - 正确格式:$b = strtotime("+7days", $a);//获取在以$a时间戳为基础的七天后的时间戳
       - 参照正确格式修改后:$timeout   = strtotime( "+ {$lockout_time} minutes",$last_login );
       */

// Check to see if enough time has passed, if it hasn't locked the account
       // 判断该锁定状态的账号,是否到达预设的解封时间。
if( $timenow > $timeout ){
//echo "<br />";
//echo "ok";
$account_locked = false;   //源码这里也错了,源码是true。
}else{
//echo "<br />";
//echo "on";
$account_locked = true;
}
}

// Check the database (if username matches the password)
   // 加入了PDO机制,通过使用PDO预处理对象来防范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();
$row = $data->fetch();

// If its a valid login...
   // 若用户登录验证成功(即提交的账号密码在数据库里存在),再进一步判断该账户是否处于锁定状态。
if( ( $data->rowCount() == 1 ) && ( $account_locked == false ) ) {
// Get users details
       // 若该账户状态为未锁定,则该用户登录成功。
$avatar       = $row[ 'avatar' ];
$failed_login = $row[ 'failed_login' ];
$last_login   = $row[ 'last_login' ];

// Login successful
$html .= "<p>Welcome to the password protected area <em>{$user}</em></p>";
$html .= "<img src="{$avatar}" />";

// Had the account been locked out since last login?
       // 若该用户之前尝试登录次数超过我们安全预设的限制次数,则在此处对用户提出一个警告。
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>";
}

// Reset bad login count
       // 用户一旦成功登录,则重置其尝试登录的次数为0
$data = $db->prepare( 'UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
}
else {
// Login failed
       // 若用户提交的账号密码在数据库里不存在、或该账户状态为锁定,则该用户登录失败。
       
       // 随机2-4秒延迟再执行当前脚本(impossible.php),即造成了2-4秒的响应延迟
       // 这样也可以一定程度上加大客户端进行暴破的代价(虽然和high级别一样鸡肋,但有总比没有好)
sleep( rand( 2, 4 ) );

// Give the user some feedback
$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>";

// Update bad login count
       // 将该用户用户登录失败次数加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();
}

// Set the last login time
   // 最后设置该用户尝试登陆的最后时间。
$data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
}

// Generate Anti-CSRF token
generateSessionToken();

?>

 

原文地址:https://www.cnblogs.com/P201821460033/p/13861361.html