文件上传
怎么感觉这个系列的文章可以共用一个模板了。。(逃
原理:
文件上传漏洞是指用户上传了一个可执行的脚本文件,
并通过此脚本文件获得了执行服务器端命令的能力。
目录:
NO.1 Low
首先来看下代码
<?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' ] );
// 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>";
}
}
?>
先学习学习函数!!!
basename()
函数返回路径中的文件名部分。
语法:basename(path,suffix)
参数:
-
path 必需。规定要检查的路径。
-
suffix 可选。规定文件扩展名。如果文件有名有文件扩展名,将不会显示这个扩展名。
move_uploaded_file()
函数将上传的文件移动到新位置。
语法: move_uploaded_file(file,newloc)
注:本函数仅用于通过 HTTP POST 上传的文件。
参数:
-
file 必需。规定要移动的文件。
-
newloc 必需。规定文件的新位置。
$_FILES函数
$_FILES
数组内容如下:
$_FILES['myFile']['name']
客户端文件的原名称。
$_FILES['myFile']['type']
文件的 MIME 类型,需要浏览器提供该信息的支持,例如”image/gif”。
$_FILES['myFile']['size']
已上传文件的大小,单位为字节(Byte)。
$_FILES['myFile']['tmp_name']
文件被上传后在服务端储存的临时文件名
$_FILES['myFile']['error']
和该文件上传相关的错误代码。
[‘error’] 有六种值,分别是:
UPLOAD_ERR_OK
值:0; 没有错误发生,文件上传成功。
UPLOAD_ERR_INI_SIZE
值:1; 上传的文件超过了 php.ini 中 upload_max_filesize 选项限制的值。
UPLOAD_ERR_FORM_SIZE
值:2; 上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值。
UPLOAD_ERR_PARTIAL
值:3; 文件只有部分被上传。
UPLOAD_ERR_NO_FILE
值:4; 没有文件被上传。
值:5; 上传文件大小为0.
注:
文件被上传结束后,默认地被存储在了临时目录中,
这时必须将它从临时目录中删除或移动到其它地方,如果没有,则会被删除。
来解读一下代码,
if
判断有没有文件上传,
如果有的话设置一个$target_path
变量来接收文件上传的路径(hackable/uploads/
),
然后使用basename()
函数获取上传的文件名,然后再拼接上传的路径,
为了方便看可以换成这样,
$target_path = $target_path.basename( $_FILES[ 'uploaded' ][ 'name' ] );
假如上传一个名为shell.php的文件,
$target_path
依然是不变的,为hackable/uploads/
然后basename()
函数获取文件名就为shell.php
拼接起来就是hackable/uploads/shell.php
这段代码没有对上传的文件后缀做任何的判断,
导致任意文件上传,
这里直接上传php后缀的一句话即可
上传成功后返回的路径
NO.2 Medium
代码,
<?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 ) ) { // 100kb
// 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>';
}
}
?>
这段代码也是挺简单的,
加了三个判断,判断类型是不是jpg
,png
,还有文件不能大于100kb
这里依旧没有判断文件后缀,只是检测了MIME
类型,也就是这个东西,
这里直接抓包将Content-Type
值改为image/jpeg
即可绕过
这种方法在渗透测试中也经常使用,
另外,如果不修改mime类型的话,此题没有其他绕过方法了。
NO.3 High
<?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_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 '<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>';
}
}
?>
substr()
函数返回字符串的一部分。
语法: substr(string,start,length)
参数:
string 必需。规定要返回其中一部分的字符串。
start 必需。规定在字符串的何处开始。
正数 - 在字符串的指定位置开始
负数 - 在从字符串结尾的指定位置开始
0 - 在字符串中的第一个字符处开始
length 可选。规定要返回的字符串长度。默认是直到字符串的结尾。
正数 - 从 start 参数所在的位置返回
负数 - 从字符串末端返回
strrpos()
函数查找字符串在另一字符串中最后一次出现的位置(区分大小写)。
语法: strrpos(string,find,start)
参数:
- string 必需。规定被搜索的字符串。
- find 必需。规定要查找的字符。
- start 可选。规定开始搜索的位置。
例: 查找 “php” 在字符串中最后一次出现的位置 输出19
<?php
echo strrpos("I love php, I love php too!","php");
?>
相关函数:
- strpos() - 查找字符串在另一字符串中第一次出现的位置(区分大小写)
- stripos() - 查找字符串在另一字符串中第一次出现的位置(不区分大小写)
- strripos() - 查找字符串在另一字符串中最后一次出现的位置(不区分大小写)
strtolower()
函数把字符串转换为小写。
语法: strtolower(string)
注:该函数是二进制安全的。
- 相关函数:
- lcfirst() - 把字符串中的首字符转换为小写
- strtoupper() - 把字符串转换为大写
- ucfirst() - 把字符串中的首字符转换为大写
- ucwords() - 把字符串中每个单词的首字符转换为大写
getimagesize()
函数用于获取图像大小及相关信息。
语法: array getimagesize ( string $filename [, array &$imageinfo ] )
这里的话是判断文件头是否为图片。
先来详解一下这段代码:
$uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
变量$uploaded_name
接收上传的文件名,假如上传一个shell.php文件
先看看strrpos
函数
上传文件后就会变成 strrpos( 'shell.php', '.' )
,
意思是查找.
出现的文章,然后会输出5,点的前一位,
再来看看substr
函数
然后就会变成 substr( "shell.php", 5 + 1)
,为什么要加一呢?
如果是5的话,则会截取.php
,不包括5,会从第六位开始截取,
6的话则会截取到php
,当然整段代码意思也很明确,就是获取文件后缀。
这个级别除了判断文件后缀之外,还判断了文件头,
普通一句话改成jpg都过不了了,
这里我们可以使用一句话图片马,
合成语法为: copy x.jpg /b + b.txt /a c.jpg
注意: x.jpg为图片(png,gif都行) ,b.txt为一句话 ,c.gif为最后欲生成的文件名
然后打开编辑器可以看到一句话
然后这里直接以jpg
后缀上传,然后采用文件包含的方式访问,
http://localhost/dvwa/vulnerabilities/fi/?page=file:///D:\phpStudy\PHPTutorial\WWW\dvwa\hackable\uploads\c.jpg
发现可以解析
然后上菜刀就行了,注意: 这里的话可能需要菜刀上的浏览器登录一下dvwa才能连接shell,
浏览器左上角菜单栏可以打开,
本菜这里环境有点问题,就不演示了,感兴趣的朋友可以去试试。。
附1:文件上传可控点
- Content-Length,即上传内容大小
- MAX_FILE_SIZE,即上传内容的最大长度
- filename,即上传的文件名
- 上传的文件内容
- 请求包可控点可能存在上传路径
附2:上传绕过–客户端
- 用firebug将form表单中的onsubmit事件删除
- 上传文件,抓包拦截,修改数据
这些例子只是个参考,更多方法还要靠自己去研究。
附3:上传绕过–服务端
- 黑白名单过滤
- 修改MIME类型
- 截断上传
- .htaccess文件攻击
- 目录验证
总结
这些题目还是很局限性的,
有些是因为自己环境问题完成不了一些操作,
同时,题目的设计导致很多方法都不能够测试,
不过上传还是有很多绕过方法的,
比如配合解析漏洞等等
参考文献
https://www.freebuf.com/articles/web/119467.html
https://www.freebuf.com/vuls/128846.html