从 php 代码层面分析文件上传漏洞

从 PHP 代码层面分析文件上传漏洞

  • 根据 Upload-Labs 来进行总结

0x00.参考文章

1
2
3
4
<?php
$b = $_GET['a'];
include($b);
?>

0x01.从客户端 JS 去限制文件扩展名(本地校验)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function checkFile() {
var file = document.getElementsByName('upload_file')[0].value;
if (file == null || file == "") {
alert("请选择要上传的文件!");
return false;
}
//定义允许上传的文件类型
var allow_ext = ".jpg|.png|.gif";
//提取上传文件的类型
var ext_name = file.substring(file.lastIndexOf("."));
//判断上传文件类型是否允许上传
if (allow_ext.indexOf(ext_name + "|") == -1) {
var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
alert(errMsg);
return false;
}
}

Bypass

  • 删除关键白名单 JS 代码片段

  • 在白名单里加入恶意扩展名

0x02.检查数据包的 MIME(文件类型)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
//这一段
if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) {
//这一段

$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' . $_FILES['upload_file']['name']
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '文件类型不正确,请重新上传!';
}
} else {
$msg = UPLOAD_PATH.'文件夹不存在,请手工创建!';
}
}

即使用$_FILES['upload_file']['type'] == 'image/jpeg'来进行数据包传输的限制

Bypass

  • 仅将数据包中的 Content-Type 修改为合法类型,文件扩展名不变

0x03.对扩展名进行黑名单限制

  • 模板代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {

//黑名单规则部分

$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");//进行黑名单限制
$file_name = trim($_FILES['upload_file']['name']);
$file_ext = strrchr($file_name, '.');

$file_ext = strtolower($file_ext); //转换为小写

$file_ext = trim($file_ext); //收尾去空

$file_name = deldot($file_name);//删除文件名末尾的点

$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA

$file_name = str_ireplace($deny_ext,"", $file_name);//对恶意扩展名进行替换操作

//黑名单规则部分

if(!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file,$img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

1.规则-扩展名进行黑名单限制

代码

1
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");

Bypass

  • 使用一些别名,例如 php 则可以使用

php2
php3
php4
php5
phps
pht
phtm
phtml

  • 使用 Apache 的.htaccess 规则

通过构造当前网页配置文件.htaccess

创建一个文件名为.htaccess 的文件,保存格式为 eXtensible Markup Language file
内容为:

1
2
3
4
5
6
<FilesMatch "cimer"> //cimer可以替换为自己想要的扩展名,后期改这个扩展名上传就好了
SetHandler application/x-httpd-php
</FilesMatch>

将.htaccess上传到指定页面中(它是配置文件,所以不会被拦截),然后将自己的.php代码更改扩展名名为.cimer(自己起的那个)上传
菜刀连接时同样访问这个****.cimer就好。
  • 使用 Apache 从右向左解析的规则

上传 1.php.bak 或者任意一个编造的扩展名
在某些版本可当做 php 来执行(因为 apache 从右到左对文件名进行解析,而 .bak 是 httpd.conf 识别不了的文件名扩展名,
故 apache 会自动提取左边的.php)

2.规则-扩展名大小写限制

代码

1
$file_ext = strtolower($file_ext);//转换为小写

Bypass

  • 如果没有大小写限制,那么我们就可以使用 pHp 来进行绕过

3.规则-扩展名首尾去空

代码

1
$file_ext = trim($file_ext);//对扩展名进行首尾去空操作

Bypass

  • 如果没有收尾去空操作,那么我们就可以在扩展名后加空格进行绕过

4.规则-删除末尾的点

代码

1
$file_name = deldot($file_name);//删除末尾的点

Bypass

  • 如果没有删除末尾的点,那么我们就可以使用 1.php.进行绕过

  • Windows 环境下会自动删除最后的点

5.规则-进行文件流限制

代码

1
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA

Bypass

  • 如果没有进行文件流限制,那么我们可以使用上传文件名是 1.php::$DATA 来进行绕过(访问时去除文件流后缀)

  • 1.php:1.txt 是正常文件流,而:$DATA 是当前文件默认文件流

6.规则-对恶意扩展名进行替换

代码

1
$file_name = str_ireplace($deny_ext,"", $file_name);//对恶意扩展名进行替换操作

Bypass

  • 注意,即便有这样的替换操作,我们依旧可以使用双写进行绕过

0x04.上传路径可控

1.通过 GET 方式控制(URL)

代码

1
2
3
4
5
6
index.php
<form action="?save_path=../upload/" enctype="multipart/form-data" method="post"><!--就是这里的?save_path=-->
<p>请选择要上传的图片:<p>
<input class="input_file" type="file" name="upload_file"/>
<input class="button" type="submit" name="submit" value="上传"/>
</form>

Bypass(%00 截断)

  • 如果路径可控,我们可以先上传一个有一句话的合法类型文件(JPG/GIF 等)

  • 然后我们在可控制路径的地方使用%00 截断,具体操作操作例如将路径更换为你要上传马的路径加上 php 文件名:../upload/1.php%00

  • 然后上传上去就是 1.php,原先的 jpg 不会被上传

2.通过 POST 方式控制

代码

1
2
3
4
5
6
7
index.php
<form enctype="multipart/form-data" method="post">
<p>请选择要上传的图片:<p>
<input type="hidden" name="save_path" value="../upload/"/>
<input class="input_file" type="file" name="upload_file"/>
<input class="button" type="submit" name="submit" value="上传"/>
</form>

Bypass(0x00 截断)

  • 和 GET 方式大致相似,但在可控路径上添加“一个空格与 a”:../upload/1.php a

  • 空格与 a 的 HEX 分别是 20、61 将 20 改为 00,即可绕过

0x05.检查图片内容

1.只检查头两个字节

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function getReailFileType($filename){
$file = fopen($filename, "rb");
$bin = fread($file, 2); //只读2字节
fclose($file);
$strInfo = @unpack("C2chars", $bin);
$typeCode = intval($strInfo['chars1'].$strInfo['chars2']);
$fileType = '';
switch($typeCode){
case 255216:
$fileType = 'jpg';
break;
case 13780:
$fileType = 'png';
break;
case 7173:
$fileType = 'gif';
break;
default:
$fileType = 'unknown';
}
return $fileType;
}

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_type = getReailFileType($temp_file);

if($file_type == 'unknown'){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$file_type;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}

Bypass

  • 只用在 php 文件前加上一行标识

GIF:GIF89a
JPG:ff, d8(HEX 里边修改)
PNG:89 50 4e 47 0d 0a 1a 0a(HEX 里边修改)

2.通过特别函数来检查图片内容

利用 getimagesize()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function isImage($filename){
$types = '.jpeg|.png|.gif';
if(file_exists($filename)){
$info = getimagesize($filename);//使用getimagesize()检查图片
$ext = image_type_to_extension($info[2]);
if(stripos($types,$ext)>=0){
return $ext;
}else{
return false;
}
}else{
return false;
}
}

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$res = isImage($temp_file);
if(!$res){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").$res;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}

利用 exif_imagetype()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function isImage($filename){
//需要开启php_exif模块
$image_type = exif_imagetype($filename);//通过exif_imagetype()来检查图片内容
switch ($image_type) {
case IMAGETYPE_GIF:
return "gif";
break;
case IMAGETYPE_JPEG:
return "jpg";
break;
case IMAGETYPE_PNG:
return "png";
break;
default:
return false;
break;
}
}

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$res = isImage($temp_file);
if(!$res){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$res;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}

Bypass

  • 只用上传个在末尾有加 shell 代码的图片马就好了

0x06.将上传的图片进行重新渲染

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
关键代码
//截取相关后缀名操作

if(($fileext == "jpg") && ($filetype=="image/jpeg")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefromjpeg($target_path);
if($im == false){
$msg = "该文件不是jpg格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".jpg";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagejpeg($im,$img_path);
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}

else if(($fileext == "png") && ($filetype=="image/png")){
//省略了一堆代码
$im = imagecreatefromjpeg($target_path);
}

else if(($fileext == "gif") && ($filetype=="image/gif")){
//省略了一堆代码
$im = imagecreatefromjpeg($target_path);
}

Bypass

  • GIF

先上传 GIF,然后将上传的图片下载下来
用可以看 16 进制的编辑器进行对比,在未变化的部分加入 shell 代码

  • PNG

参考上面的文章

  • JPG

使用 [[JPG_Payload.php]] 这个脚本,注意此脚本最好去传比较小一些的文件,太大了会将 shell 插入错地方导致失败,而且多试几张图片

Author

Resek4

Posted on

2020-01-11

Updated on

2023-02-26

Licensed under

Comments