DVWA是一个适合新手锻炼的靶机,是由PHP/MySQL组成的 Web应用程序,帮助大家了解web应用的攻击手段
DVWA大致能分成以下几个模块,包含了OWASP Top 10大主流漏洞环境。
Brute Force——暴力破解
Command Injection——命令注入
CSRF——跨站请求伪造
File Inclusion——文件包含
File Upload——文件上传漏洞
Insecure CAPTCHA——不安全的验证
SQL Injection——sql注入
SQL Injection(Blind)——sql注入(盲注)
Weak Session IDs——不安全的SessionID
XSS(DOM)——DOM型XSS
XSS(Reflected)——反射型XSS
XSS(Stored)——存储型XSS
CSP Bypass ——绕过内容安全策略
JavaScript——JS攻击
本文所用dvwa版本为2.3
GitHub - digininja/DVWA: Damn Vulnerable Web Application (DVWA)
可以自行通过phpstudy搭建以供练习
DVWA Security分为四个等级,low,medium,high,impossible,不同的安全等级对应了不同的漏洞等级。
1.low-最低安全等级,无任何安全措施,通过简单毫无安全性的编码展示了安全漏洞。
2.medium-中等安全等级,有安全措施的保护,但是安全性并不强。
3.high-高等安全等级,有安全措施保护,是中等安全等级的加深,这个等级下的安全漏洞可能不允许相同程度的攻击。
4.impossible-最高(不可能)安全等级,存在相当强的安全保护措施,这个级别应能防止所有的漏洞,主要用于比较不同等级下的源代码。
开此篇文章的主要目的是为了对代码进行分析,查看其应对攻击的方法及绕过手法
进入暴力破解的界面,有点基础的都知道,在bp中通过字典,导入用户名和密码进行爆破,这样的操作大家见多了,再写没啥意思,想看的可以去pikachu靶场的暴力破解去了解具体操作,本文主要结合代码进行分析
pikachu靶场-暴力破解-CSDN博客
low等级
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// Get username
$user = $_GET[ 'username' ]; //通过get方式获取username
// Get password
$pass = $_GET[ 'password' ]; //通过get方式获取password
$pass = md5( $pass ); //收到密码后进行md5加密
// 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 ) {
//mysqli_num_rows()函数是一个用于获取查询结果中行数的mysqli扩展函数。它接受一个查询结果对象作为参数,并返回该结果集中的行数。通常用于检查查询结果是否包含了符合条件的记录,以便在代码中进行相应的处理。
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
// 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);
}
?>
以上是一个简单的PHP登录验证功能。当用户提交登录表单时,通过GET方法获取用户名和密码,然后将密码进行MD5加密。接着查询数据库,检查用户名和加密后的密码是否匹配数据库中的记录。如果匹配成功,则显示用户的欢迎信息和头像;如果匹配失败,则显示错误信息。
无任何防护,直接进行暴力破解即可
medium等级
<?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
$html .= "<p>Welcome to the password protected area {$user}</p>";
$html .= "<img src=\"{$avatar}\" />";
}
else {
// Login failed
sleep( 2 );
$html .= "<pre><br />Username and/or password incorrect.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
相比于low的代码,这里获取到username和password后,有一段转义的代码
$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)) ? "" : ""));
这行代码是对用户输入的用户名进行转义处理,以防止SQL注入攻击。具体来说,它使用了mysqli_real_escape_string()函数对用户输入的用户名进行转义,确保特殊字符在SQL语句中不会被误解为SQL语句的一部分,从而保护数据库安全。
加了这个转义只是对sql注入进行防范,对暴力破解还是没有起到效果
high等级
<?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
$html .= "<p>Welcome to the password protected area {$user}</p>";
$html .= "<img src=\"{$avatar}\" />";
}
else {
// Login failed
sleep( rand( 0, 3 ) );
$html .= "<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
选中这个checktoken函数,跳转到这个函数
// Token functions --
function checkToken( $user_token, $session_token, $returnURL ) { # Validate the given (CSRF) token
global $_DVWA;
if (in_array("disable_authentication", $_DVWA) && $_DVWA['disable_authentication']) {
return true;
}
if( $user_token !== $session_token || !isset( $session_token ) ) {
dvwaMessagePush( 'CSRF token is incorrect' );
dvwaRedirect( $returnURL );
}
}
跟进到checkToken方法,设置了三个参数,如果user_token不等于session_token或者session_token为空,调用dvwaRedirect方法。
function dvwaRedirect( $pLocation ) {
session_commit();
header( "Location: {$pLocation}" );
exit;
}
这段代码定义了一个名为dvwaRedirect()
的函数,用于实现页面重定向的功能。函数接受一个参数$pLocation
,即要重定向到的目标位置。
在函数内部,首先调用session_commit()
函数来提交并保存当前会话的所有数据。然后通过header()
函数设置HTTP响应头,将浏览器重定向到指定的目标位置。最后使用exit
函数来终止脚本的执行,确保页面重定向的立即生效。
这个函数通常用于在应用程序中需要进行页面跳转的情况,如登录成功后跳转到用户首页,或者在某些条件下需要跳转到其他页面。通过调用这个函数,可以实现简单的页面重定向功能。
可以看到这个提交界面多了一串user_token,并且每次刷新都重新生成,pikachu靶场的那篇文章有演示在前端发现token怎么进行爆破操作
从medium等级开始,登录失败就会延时,提高了暴力破解的成本
impossiable等级
<?php
if( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {
// 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;
// Check the database (Check user information)
$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 ) ) {
// User locked out. Note, using this method would allow for user enumeration!
//$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
$last_login = strtotime( $row[ 'last_login' ] );
$timeout = $last_login + ($lockout_time * 60);
$timenow = time();
/*
print "The last login was: " . date ("h:i:s", $last_login) . "<br />";
print "The timenow is: " . date ("h:i:s", $timenow) . "<br />";
print "The timeout is: " . date ("h:i:s", $timeout) . "<br />";
*/
// Check to see if enough time has passed, if it hasn't locked the account
if( $timenow < $timeout ) {
$account_locked = true;
// print "The account is locked<br />";
}
}
// Check the database (if username matches the password)
$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
$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
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
$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();
?>
这是最高难度的代码,它主要采取了以下几种措施来防止暴力破解
POST方式获取用户名密码
设置token
对sql语句进行转义
PDO预编译
设置登录次数,超过次数进行锁定
登录失败后延时
一般来说,这种情况进行爆破是不可能的了,或者说成本过高