CSRF,全称Cross-site request forgery,翻译过来就是跨站请求伪造,是指利用受害者尚未失效的身份认证信息(cookie、会话等),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下以受害者的身份向(身份认证信息所对应的)服务器发送请求,从而完成非法操作(如转账、改密等)。CSRF与XSS最大的区别就在于,CSRF并没有盗取cookie而是直接利用。
if( isset( $_GET[ 'Change' ] ) ) {
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Do the passwords 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 the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( ''
. ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
' );
// Feedback for the user
echo "Password Changed."; } else { // Issue with passwords matching echo "
Passwords did not match."; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>
// Do the passwords 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 );
服务器收到修改密码的请求后,会检查参数
password_new
与password_conf
是否相同,如果相同,就会修改密码,并没有任何的防CSRF机制(当然服务器对请求的发送者是做了身份验证的,是检查的cookie,只是这里的代码没有体现)。
需要注意的是,CSRF最关键的是利用受害者的cookie向服务器发送伪造请求,所以如果受害者之前用Chrome浏览器登录的这个系统,而用搜狗浏览器点击这个链接,攻击是不会触发的,因为搜狗浏览器并不能利用Chrome浏览器的cookie,所以会自动跳转到登录界面。
构造链接http://127.0.0.1/dvwa/vulnerabilities/csrf/?password_new=admin&password_conf=admin&Change=Change#
就是访问这个链接密码就会改变
if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Do the passwords 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 the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( ''
. ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
' );
// Feedback for the user
echo "Password Changed."; } else { // Issue with passwords matching echo "
Passwords did not match."; } } else { // Didn't come from a trusted source echo "
That request didn't look correct."; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>
相关函数学习
stripos() 函数
查找字符串在另一字符串中第一次出现的位置(不区分大小写)。
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false )
这句代码就是利用stripos() 函数
进行HTTP_REFERER
(http数据包头的Referer参数的值,表示来源地址)和SERVER_NAME
(http数据包头的Host参数,及要访问的主机名)的匹配检查
抓包查看
过滤规则是http包头的Referer参数的值中必须包含主机名(这里是127.0.0.1)
可以将攻击页面命名为127.0.0.1.html(页面被放置在攻击者的服务器里)就可以绕过
high
if( isset( $_GET[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Do the passwords 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 the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( ''
. ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
' );
// Feedback for the user
echo "Password Changed."; } else { // Issue with passwords matching echo "
Passwords did not match."; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } // Generate Anti-CSRF token generateSessionToken(); ?>
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
可以发现加入了Anti-CSRF token机制,用户每次访问改密页面时,服务器会返回一个随机的token,向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。
要绕过High级别的反CSRF机制,关键是要获取token,要利用受害者的cookie去修改密码的页面获取关键的token。
可以尝试去构造一个攻击页面,将其放置在攻击者的服务器,引诱受害者访问,从而完成CSRF攻击
impossible
if( isset( $_GET[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$pass_curr = $_GET[ 'password_current' ];
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// 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
echo "Password Changed.
";
}
else {
// Issue with passwords matching
echo "Passwords did not match or current password incorrect.
";
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
Impossible级别的代码利用PDO技术防御SQL注入,至于防护CSRF,则要求用户输入原始密码(简单粗暴),攻击者在不知道原始密码的情况下,无论如何都无法进行CSRF攻击。
学习PDO技术防范SQL注入的方法
File Inclusion,意思是文件包含(漏洞),是指当服务器开启allow_url_include选项时,就可以通过php的某些特性函数(include(),require()和include_once(),require_once())利用url去动态包含文件,此时如果没有对文件来源进行严格审查,就会导致任意文件读取或者任意命令执行。文件包含漏洞分为本地文件包含漏洞与远程文件包含漏洞,远程文件包含漏洞是因为开启了php配置中的allow_url_fopen选项(选项开启之后,服务器允许包含一个远程的文件)。
// The page we wish to display
$file = $_GET[ 'page' ];
?>
服务器端对page参数没有做任何的过滤跟检查。
服务器期望用户的操作是点击下面的三个链接,服务器会包含相应的文件,并将结果返回。需要特别说明的是,服务器包含文件时,不管文件后缀是否是php,都会尝试当做php文件执行,如果文件内容确为php,则会正常执行并返回结果,如果不是,则会原封不动地打印文件内容,所以文件包含漏洞常常会导致任意文件读取与任意命令执行。
本地文件包含
http://127.0.0.1/dvwa/vulnerabilities/fi/?page=/etc/shadow
Warning: include(/etc/shadow): failed to open stream: No such file or directory in D:\phpstudy_pro\WWW\dvwa\vulnerabilities\fi\index.php on line 36
Warning: include(): Failed opening '/etc/shadow' for inclusion (include_path='.;C:\php\pear;../../external/phpids/0.6/lib/') in D:\phpstudy_pro\WWW\dvwa\vulnerabilities\fi\index.php on line 36
报错显示没有这个文件,同时暴露了服务器文件的绝对路径D:\phpstudy_pro\WWW
构造绝对路径
http://127.0.0.1/dvwa/vulnerabilities/fi/?page=D:\phpstudy_pro\WWW\dvwa\php.ini
; This file attempts to overwrite the original php.ini file. Doesnt always work. magic_quotes_gpc = Off allow_url_fopen = On allow_url_include = On
配置文件中的magic_quote_gpc
选项为off。在php版本小于5.3.4的服务器中,当magic_quote_gpc
选项为off时,我们可以在文件名中使用%00
进行截断,也就是说文件名中%00
后的内容不会被识别
使用%00
截断可以绕过某些过滤规则,例如要求page参数的后缀必须为php
medium
// The page we wish to display
$file = $_GET[ 'page' ];
// Input validation
$file = str_replace( array( "http://", "https://" ), "", $file );
$file = str_replace( array( "../", "..\"" ), "", $file );
?>
对比low的代码增加了str_replace
函数,对page参数进行替换的处理,将http://
、https://
、 ../
、..\
替换为空字符
可以使用双写绕过替换规则
因为替换的只是../
、..\
,所以对采用绝对路径的方式包含文件是不会受到任何限制
high
// The page we wish to display
$file = $_GET[ 'page' ];
// Input validation
if( !fnmatch( "file*", $file ) && $file != "include.php" ) {
// This isn't the page we want!
echo "ERROR: File not found!";
exit;
}
?>
对比low使用了fnmatch函数
检查page参数,要求page参数的开头必须是file
,服务器才会去包含相应的文件
可以用浏览器打开一个本地文件,利用了file协议
http://127.0.0.1/dvwa/vulnerabilities/fi/?page= file:///xxx
Impossible
// The page we wish to display
$file = $_GET[ 'page' ];
// Only allow include.php or file{1..3}.php
if( $file != "include.php" && $file != "file1.php" && $file != "file2.php" && $file != "file3.php" ) {
// This isn't the page we want!
echo "ERROR: File not found!";
exit;
}
?>
Impossible级别的代码使用了白名单机制进行防护,简单粗暴,page参数必须为“include.php”、“file1.php”、“file2.php”、“file3.php”之一,彻底杜绝了文件包含漏洞
文件上传漏洞,由于对上传文件的类型、内容没有进行严格的过滤、检查,可以通过上传木马获取服务器的webshell权限
查看源码
if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
// Can we move the file to the upload folder?
if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
// No
echo 'Your image was not uploaded.
';
}
else {
// Yes!
echo "{
$target_path} succesfully uploaded!
";
}
}
?>
medium
if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
// File information
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
// 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
echo 'Your image was not uploaded.
';
}
else {
// Yes!
echo "{
$target_path} succesfully uploaded!
";
}
}
else {
// Invalid file
echo 'Your image was not uploaded. We can only accept JPEG or PNG images.
';
}
}
?>
// Is it an image?
if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&( $uploaded_size < 100000 ) )
代码对上传文件的类型、大小做了限制,要求文件类型必须是jpeg或者image,大小不能超过100000B
既然让上传jpeg或者image,那么可以通过修改文件后缀来实现绕过
if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
// File information
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
$uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ];
// Is it an image?
if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
( $uploaded_size < 100000 ) &&
getimagesize( $uploaded_tmp ) ) {
// Can we move the file to the upload folder?
if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) {
// No
echo 'Your image was not uploaded.
';
}
else {
// Yes!
echo "{
$target_path} succesfully uploaded!
";
}
}
else {
// Invalid file
echo 'Your image was not uploaded. We can only accept JPEG or PNG images.
';
}
}
?>
相关函数学习
strrpos(string,find,start)
函数返回字符串find在另一字符串string中最后一次出现的位置,如果没有找到字符串则返回false,可选参数start规定在何处开始搜索。
getimagesize(string filename)
函数会通过读取文件头,返回图片的长、宽等信息,如果没有相关的图片文件头,函数会报错。
可以看到,High级别的代码读取文件名中最后一个”.”后的字符串,期望通过文件名来限制文件类型,因此要求上传文件名形式必须是”.jpg”、”.jpeg”、”*.png”之一。同时,getimagesize函数更是限制了上传文件的文件头必须为图像类型。
就是要检查文件头,直接添加一个即可
if( isset( $_POST[ 'Upload' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// File information
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
$uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ];
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . 'hackable/uploads/';
//$target_file = basename( $uploaded_name, '.' . $uploaded_ext ) . '-';
$target_file = md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;
$temp_file = ( ( ini_get( 'upload_tmp_dir' ) == '' ) ? ( sys_get_temp_dir() ) : ( ini_get( 'upload_tmp_dir' ) ) );
$temp_file .= DIRECTORY_SEPARATOR . md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;
// Is it an image?
if( ( strtolower( $uploaded_ext ) == 'jpg' || strtolower( $uploaded_ext ) == 'jpeg' || strtolower( $uploaded_ext ) == 'png' ) &&
( $uploaded_size < 100000 ) &&
( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png' ) &&
getimagesize( $uploaded_tmp ) ) {
// 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!
echo "${target_file} succesfully uploaded!
";
}
else {
// No
echo 'Your image was not uploaded.
';
}
// Delete any temp files
if( file_exists( $temp_file ) )
unlink( $temp_file );
}
else {
// Invalid file
echo 'Your image was not uploaded. We can only accept JPEG or PNG images.
';
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
相关函数学习
in_get(varname)
函数返回相应选项的值
imagecreatefromjpeg ( filename )
函数返回图片文件的图像标识,失败返回false
imagejpeg ( image , filename , quality)
从image图像以filename为文件名创建一个JPEG图像,可选参数quality,范围从 0(最差质量,文件更小)到
100(最佳质量,文件最大)
imagedestroy( img )
函数销毁图像资源
Impossible级别的代码对上传文件进行了重命名(为md5值,导致%00截断无法绕过过滤规则),加入Anti-CSRF token防护CSRF攻击,同时对文件的内容作了严格的检查,导致攻击者无法上传含有恶意脚本的文件。
8.5