前提知识

文件上传知识

php文件上传过程

Less-1(前端禁用JS绕过)

前端存在js代码判断文件类型,所以直接浏览器禁用Js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script type="text/javascript">
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;
}
}
</script>

然后就可以直接上传一句话,注意upload文件夹在网站根目录。

Less-2(检查文件类型,MIME绕过)

后端检测文件类型,使用图片马,抓包时将filename的后缀改为.php

抓包截图

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

造成原因:后端只检测了文件的类型但是并没有检测文件后缀,如果抓包修改了文件后缀文件类型仍是图片类型,所以可以造成文件上传漏洞。

修复方案:在后端添加一个文件后缀检测函数,可以在代码层面避免,但是也会存在服务器解析漏洞不是代码层面能够解决的东西。

Less-3(服务器对陌生后缀名解析)

首先直接上传了php文件不出意外的失败了,又尝试图片马抓包修改后缀发现也失败,应该就像之前说的在后端添加了检测文件后缀的代码,但是没有检测文件头,所以不用在文件头添加幻数。

想想之前学的一共有两个思路:一个是利用phtml后缀的文件看看能不能过黑名单,还有一个就是利用服务器解析漏洞,但是这个是有条件的,具体条件忘了慢慢尝试。

能够上传.phtml后缀的文件,但是服务器不能后解析?不知道为什么,抓包的时候发现了这个东西

抓包记录

文件名让人给改了,看这个格式估计是改成了时间戳,再次进入upload目录访问这个文件解析成功了,但是蚁剑连不上,我之后有往里面添加了phpinfo()也没有解析出来,实在是奇怪。尝试用另一种方法,看看有没有服务器解析漏洞。都不行,上网搜了搜,发现是apache的配置文件中没有设置可以解析phtml,php5,php3等文件后缀,需要设置一下。

记录一下,由于docker中的系统是ubuntu的,所以apache的默认安装路径为/etc/apache2,配置文件也不再是httpd.conf而是apache2.conf,然后docker中又不能安装vim,也没有vi????,最后使用命令echo "AddType application/x-httpd-php .php .phtml .phps .php5 .pht" >> apache2.conf,重启container配置成功,php3 php5 phtml后缀文件全部都能解析。

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
//作者源码

$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']);
$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.'/'.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 . '文件夹不存在,请手工创建!';
}
}

总结:第三关要学习的即使绕过黑名单,在实际中黑名单永远比白名单好绕过,一个是不能上传哪些,一个是只能上传哪些,当过滤不严格时可以用phtml,php5,php3,pht等后缀文件上传绕过。

Less-4(使用.htaccess绕过黑名单)

使用.htaccess后文件来绕过,使他可以将其他后缀的文件解析成php文件,后端源码黑名单中过滤了很多东西。

1
AddType application/x-httpd-php .png     (把.png文件当做.php文件来解析)

上传上去,然后在上传png后缀的后门

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
# 源码

$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");
$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.'/'.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 . '文件夹不存在,请手工创建!';
}
}

Less-5(后缀面大小写绕过)

禁止了之前关卡所有的绕过方式,看提示和网上教程,使用文件名后缀大小写方式绕过。但是服务器解析不了???,后缀不正常的都解析不了,这个配置是怎么配置的。查看github项目,修改了docker-php.conf的一些东西,发现可以了??,奇怪得很。

github

但是如果直接在浏览器的url处输入会把所有的输入变成小写,需要用到hackbar,这下可以解析大小写后缀的东西了。

注意如果使用文件后缀大小写绕过最好用hackbar,不要直接输入,会将你的输入全部自动变成小写。

Less-6(Winodws下文件后缀空格绕过)

使用空格绕过,源码中少了这个语句,那么就可以使用bp抓包给文件后缀添加空格。

1
$file_ext = trim($file_ext); //首尾去空

但是这道题有一个坑,就是我用的是Docker,当文件后缀有空格时在Windows系统下会自动忽略但是Linux不会自动忽略文件末尾的空格,所以就算能够成功上传还是无法解析,最后我发现需要修改一下docker-php.conf文件(如何使用docker命令进入容器shell请自己百度),用到.htaccess的知识。在文件中添加这样一个语句。这样就可以解析了,问题暂时解决了。

1
2
3
<FilesMatch \.php >
SetHandler application/x-httpd-php
</FilesMatch>

网上的教程基本都是用phpstudy之类的集成环境在windows下搭建的,如果用docker还是有很多坑的,需要自己慢慢去踩,不过还是可以学到一点服务器的东西hhhhh

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
# 还是贴下源码

$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 = $_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 . '文件夹不存在,请手工创建!';
}
}

Less-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");
$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 . '文件夹不存在,请手工创建!';
}
}

Less-8(Windows下::$DATA文件流绕过)

这关使用多文件后缀可过,其实之前的几关都可以使用多文件后缀的方式过,但是感觉多文件后缀的方式不好,没有达到作者的意思,通过源码发现这次比之前少了一个检测。

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

关于$DATA的知识点:php在window的时候如果文件名+"::$DATA"会把::$DATA之后的数据当成文件流处理,不会检测后缀名,且保持”::$DATA“之前的文件名 他的目的就是不检查后缀名。

那么还是就可以通过抓包修改文件后缀的方式来进行绕过,先尝试尝试,发现可以上传,但是在docker中似乎只是单纯的修改了文件名,并没有任何作用

感觉这个upload-labs有部分环境都只能是在windows下才能生效,如果在Linux下很多解析规则都不一样了。这就没法做了,先跳过,之后在windows的环境下在做做看

Less-9(多点空格绕过)

这一关也还是可以通过服务器多重解析来绕过,不重点去讲,之前学文件上传的经验基本用完了,看看源码有哪些操作。

1
2
3
4
5
$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); //首尾去空

上面的过滤把之前绕过的方法全部过滤了,看网上的做法是使用空格加点来进行绕过,名称叫多点空格绕过

首先还是要通过bp抓包,然后修改后缀加空格和点。

image-20211003013728352

由于前面加了空格,所以php只会过滤文件后缀最后的点,上传到服务器上面时后缀就变成了.php.\r,这种后缀的php文件服务器是可以解析的。

上传文件格式

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
// 源码

$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 . '文件夹不存在,请手工创建!';
}
}

Less-10(正则匹配双写绕过)

加入了正则匹配,这关如果了解过sql注入就比较简单,代码逻辑就是将黑名单中的后缀名进行正则匹配,如果匹配成功就替换成空。比如我们上传一个php后缀的文件。

上传到服务器后就变成了如下的样子,由于后缀在黑名单中,所以直接被去掉了。

image-20211003015018221

绕过方法也比较简单,直接双写绕过,因为一般代码中的正则匹配只匹配一次,所以双写一次就可以绕过了。

这次上传一个文件后缀为.pphphp的php代码文件,在看看服务器上的文件夹是什么样子。

服务器上传文件

这样就写入webshell了,比较简单的一关,php的真则匹配还是比较好理解的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 源码

$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 = 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 . '文件夹不存在,请手工创建!';
}
}

Less-11(%00截断符绕过-GET方式)

现在开始白名单时间,之前全都是绕过黑名单,白名单的绕过方法要比黑名单少很多。

学习的知识点是使用%00去截断,这个漏洞有前提

1
2
(1)php版本必须小于5.3.4 
(2)打开php的配置文件php-ini,将magic_quotes_gpc设置为Off

但是我docker上的php版本是5.5.38,已经不满足条件了,只有后面在windows环境下复现或者在去折腾docker(不是很愿意)

docker-php版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 源码

$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类型文件!";
}
}

# 大致逻辑为:将文件复制两份,一份用于获取文件的后缀名,一份用于重命名。

这道题给了我一点做web的启发,就是参数对于用户可控这个问题,无论是post传参数还是get传参数,很多时候这种就是突破口,后面看到参数可控一定要非常注意。

Less-12(%00截断符绕过-POST方式)

这道题好像也是使用%00截断符绕过?

看了看源码,也抓了下包,发现就是把参数save_path的提交方式从get换到了post,方法和上面的都是一样的。。

burpsuite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 源码,和Less-11几乎是一模一样的

$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类型文件!";
}
}

Less-13(检查图片文件头)

这关有三个要求,要求传三种格式的图片马都能成功解析才算过关

access

源码分成两个部分,一个是检查文件头,取出文件头的两个字节来进行判断,但是具体逻辑是取出两个字节,使用unpack函数将两个字节转换成两个十进制数的字符串形式,在将两个十进制字符串拼接转为整数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 检测文件头函数

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

首先使用010Editor编辑好三种图片马,然后就可以绕过文件头检测。

GIF

PNG

JPG

这里需要控制参数$img_path,如果控制不了即使上传成功后缀名不对服务器也无法解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$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 = "上传出错!";
}
}
}

这里看了看,好像能修改的只有date("YmdHis")这里,但是不知道可不可以修改时间戳,之前看好像浏览器可以修改。也不对,网上看了看别人写的通关Pass,好像需要使用文件包含漏洞,这个我也不会,学web就是遇到什么学什么。

后面发现作者专门写了一个文件包含漏洞来使用上传的图片马(我没看见,眼睛是真的瞎。。),这道题也了解了点关于文件包含的知识,就是不管上传什么文件,include函数都会将那个文件当成php文件来解析,这就比厉害了,就这用有文件包含和文件上传漏洞的网页直接无解

1
2
3
4
5
6
7
8
9
10
11
12
<?php
/*
本页面存在文件包含漏洞,用于测试图片马是否能正常运行!
*/
header("Content-Type:text/html;charset=utf-8");
$file = $_GET['file'];
if(isset($file)){
include $file;
}else{
show_source(__file__);
}
?>

这关算是完成了,成功的制作了三种图片马并且成功上传,也了解了文件上传的知识,还是不错的。

Less-14(PHP内置函数绕过<image_type_to_extension>)

也是开始审计代码,毕竟什么都不知道(每做一关就吧使用的后门文件保存下来,到时候在写一个工具直接用工具去扫)

审源码,function isImage作用应该是测试文件是否为图片让后返回图片后缀名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)){
return $ext;
}else{
return false;
}
}else{
return false;
}
}

getimagesize函数的返回值,随便测试了下,使用的是png格式图片,返回了六个值,使用的数组中的第二个值,代表图片的类型

var_dump

PHP开发手册所有的图片格式代表的数字

num_of_pictures

我使用上一关制作的图片马不行,就直接echo命令将php代码写在一张图片的末尾,在上传发现可以,也能正常解析(这应该是最简单的方法)

echo "<?php phpinfo(); ?>" >> 2.png

cat image

在使用有文件包含漏洞的页面发现可以正常解析图片,这一关感觉才是正宗的图片马,之前检测文件头的并不是很有感觉,虽然也比较简单。

然后在做的时候审源码还是要一边审一边自己去尝试才是学得最快的,还有查php Manual。

Less-15(PHP内置函数绕过<exif_imagetype>)

这关的检测图片函数名也叫isImage,但是写法和上一关的不一样了,用到了一个叫做exif_imagetype的函数,在php手册中是这样介绍的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}
}

php_Manual

读取图片的地一个字节然后检测标志,自己去测试下这个函数,其实就和上一关的getimagesize函数的作用差不多,都是返回代表图片后缀的后缀编号,下面贴php Manual的介绍。

image_type_value

使用14关制作的图片马就可以成功上传,然后使用文件包含漏洞去解析,emmmmm,感觉学到的东西很少唉。就感觉如果要过系统函数的检测那就直接在别的图片后面追加php代码然后使用文件上传漏洞去解析后面几关不知道是不是都能这样过。

Less-16(图片二次渲染绕过)

这次直接尝试使用之前的图片马,发现失效了,看看源码这关对图片做了什么骚操作。源码是相当长啊。里面不认识的函数写在下面

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
$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";
$newimagepath = UPLOAD_PATH.$newfilename;
imagejpeg($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.$newfilename;
@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";
$newimagepath = UPLOAD_PATH.$newfilename;
imagepng($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.$newfilename;
@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";
$newimagepath = UPLOAD_PATH.$newfilename;
imagegif($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.$newfilename;
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else{
$msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
}
}
  • basename:获取最路径字符串的基本路径(也就是文件名)

  • unlink:上传文件,用于修复靶场之前的漏洞

  • imagecreatefromxxx:通过文件路径或者url生成新图像

  • imagexxx:从路径或者url生成一个新图像

总体逻辑就是只能上传三种格式的图片,经过php内置函数的二次渲染,如果原本的图片内部存在php后门代码那么经过二次渲染之后的图片就没有后门代码了。关于二次渲染的原理不是很懂,网上也说得不是很清楚,但是可以通过原图和经过二次渲染的图片进行比较来修改图片,需要知道一点基本的图片二进制格式,在修改时知道哪里需要修改。这里参考网上一些师傅的做法。

GIF

将经过二次渲染的图片和原图进行比较,看看那些地方没有修改,然后在没有修改的地方进行添加,每一张gif图片相同处不会一样。

PNG

需要了解PNG图片的格式,才知道如何修改才能让函数检测不出来,需要计算png图片的crc值,当payload写入图片时,crc值会随着payload的长度改变。

JPG

这个需要使用一个国外大神的脚本,将payload写入.jpg格式的图片中,重新渲染的图片就会存在payload,且经过后端的代码再次二次渲染之后payload也不会消失。

Less-17(代码逻辑漏洞,条件竞争)

代码审计,看源码,第一次审计代码,然后看到了一个新名词(条件竞争),也是第一次接触条件竞争。看了网上的简单讲解好像有些明白了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$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 = '上传出错!';
}
}

源码逻辑如下:首先上传文件,在判断文件后缀名是否合法,这个操作比较关键(用网上的话说就是引狼入室),如果文件后缀名合法则修改文件名称,如果不合法则使用unlink函数删除文件。

在看网上的文章将条件竞争时总感觉少了点什么:php脚本本身是单线程,实现高并发的是服务器,这样的话应该每进行一次上传操作服务器会启用一个单独的线程来进行管理,为什么会造成条件竞争呢?

在仔细看了看网上的文章,主要过程如下:不断向服务器发送恶意数据包,代码先将文件临时存储在服务器,然后再由后端代码判断。后端判断文件是否正常需要时间,然后在后端判断操作执行之前不断的访问那个恶意文件,如果访问成功,那么恶意文件就暂时不会被删除。

简单来说原理就是:在我们进行文件读写操作时,是不能删除该文件的。

首先需要使用bp来进行多线程发包,然后在使用Python来进行监控。这里还学到一个东西,关于docker的。

1
2
3
4
5
6
7
8
9
10
11
# python监控代码

import requests

url = "http://172.17.0.2:80/upload/shell.php"
while True:
html = requests.get(url)
if html.status_code == 200:
print("YES,you upload it!")
else:
print("NO")

刚开始使用的ip地址和端口是0.0.0.0:88,当时没有想那么多,但是后面发现Pythonrequests一直报502错误,找了很久没找到原因,突然想到0.0.0.0:88docker从容器中映射到本地的ip地址,如果需要请求那么ip地址应该是容器内的真实ip地址,这个地方卡了很久。

使用命令docker inspect upload-labs查看容器详细内容,里面说明了容器的网关地址和现在的使用的IPAddress,然后在去请求,秒通。

docker inspect

用脚本跑起来后就是一考验手速的问题了,一直F5刷新页面,可以看待偶尔会出现phpinfo()页面,蚁剑连上去也是断断续续的。

Less-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
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
<?php
//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 ); //check file upload path
if( $ret != 1 ){
return $this->resultUpload( $ret );
}

$ret = $this->checkExtension();//check upload file extension
if( $ret != 1 ){
return $this->resultUpload( $ret );
}

$ret = $this->checkSize();//check upload file size
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" );

}
};
?>

源码定义了upload类,依次调用其中的方法,检测文件的顺序为:

检测目录 -> 检测文件后缀名 -> 检测文件大小 -> 检测文件是否存在 -> 将文件上传到服务器 -> 重命名文件

所以这关可以使用图片马加条件竞争的方式去Pass,首先文件后缀名需要符合要求,才能上传到服务器,这个过程利用条件竞争来拿到网站shell(手速一定要快)

使用场景与触发条件:

  • 服务器能多后缀名解析(或者其他解析漏洞),这个实在服务器层面
  • 如果服务器层面上没有解析漏洞,那么就需要代码层面有文件包含漏洞(但是如果有文件包含漏洞就不需要条件竞争了)
  • 代码逻辑为先上传,在重命名

总结:Pass-17与Pass-18都是使用条件竞争,其实条件竞争本身不是漏洞,只是在后端代码层面上出现了逻辑漏洞(先上传后判断在改名,所谓的引狼入室),在发现存在先上传后判断的代码都可测试使用条件竞争来get webshell

Less-19(上传文件名可控,多方法绕过)

这一关上传文件需要自定义文件名,文件名参数可控,那么方式就很多了。在windows和Linux上有不同的文件名称管理机制

后缀,空格,多点空格,%00截断绕过都可以,只需要在抓包的时候修改就OK。

Less-20(CTF)

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不是数组就进行分割
$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 = "请选择要上传的文件!";
}

参数$file可控,看下来经过一系列过滤后就是$img_path = UPLOAD_PATH . '/' . $file_name$file_name就是参数$file过滤后的形式。

从后往前看发现问题不大,审代码发现这样一句 if (!is_array($file)),这句话问题较大,如果$file是数组就不会进行分割