本节小编将讲述文件上传漏洞的最终进阶篇,带领大家完成文件上传漏洞的全部学习内容。
PASS-11(0x00截断)
0x00截断 (原理)
核心原理:字符串结束符的 "欺骗"
0x00 是ASCII 码中的空字符(NULL) ,对应的转义字符是\0。它的关键特性是:在 C/C++ 等底层语言中,\0是字符串的结束标志 ------ 当程序读取到\0时,会立即停止解析后续内容,认为字符串已经结束;但PHP、Java 等上层语言 ,在部分场景下(如接收、拼接、校验数据时)不会将\0当作结束符,会完整保留整个字符串。
靶场的源代码如下:
javascript
$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类型文件!";
}
}
问题就出在这行代码,
javascript
// 拼接文件存储路径:用户可控路径 + 随机数 + 时间戳 + 合法后缀
$img_path = $_GET['save_path'] . "/" . rand(10, 99) . date("YmdHis") . "." . $file_ext;
这段代码的核心漏洞点在于 **$_GET['save_path']完全可控 **,攻击者可在save_path中插入\0(0x00 空字符),利用 "PHP 校验完整字符串、底层 C 函数截断解析" 的差异,突破后缀和路径限制。
为帮助读者理解,小编将执行流程总结如下:
step1: 攻击者构造 URL 传入恶意save_path:
javascript
?save_path=upload/easy.php%00
(%00是\0的 URL 编码,PHP 接收后会自动解码为 0x00 空字符)
**step2:**PHP 层拼接路径:
javascript
$img_path = "upload/easy.php\0" . "/" . rand(10,99) . date("YmdHis") . ".jpg";
PHP 会完整保留\0,此时$file_ext仍是jpg,后端校验认为后缀合法;
step3: 底层 C 函数执行保存(如move_uploaded_file):
解析路径时遇到\0立即截断,实际保存路径变为 :upload/easy.php;
step4: 最终服务器生成easy.php恶意脚本,攻击者可远程控制。
实操步骤如下:
开启代理和BP,拦截抓包修改数据,
我们上传的是一个jpg格式文件,符合白名单的要求,

将save_path 修改为**../upload/easy.php%00** ,在拼接步骤时会拼接为easy.php%00loudong.jpg
然而存在0X00截断,最终传入easy.php文件成功。


访问图像链接时删去URL地址中easy.php部分后面的内容即可拿到靶场的全部信息,

PASS-12(Post 0x00截断)
javascript
$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类型文件!";
}
}
这是PASS-12的源代码,与PASS-11进行对比,我们发现save_path 从GET 请求变成了POST请求
javascript
$img_path = $_GET['save_path'] . "/" . rand(10, 99) . date("YmdHis") . "." . $file_ext;
$img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
GET型提交的内容会被⾃动进⾏URL解码,在POST请求中,%00不会被⾃动解码,这也导致我们增加了一个URL解码步骤,如图:


其他步骤与PASS-11大同小异,此处略去。
PASS-13(图**⽚⽊⻢**1)
javascript
function getReailFileType($filename){
// 以二进制只读模式打开文件(rb:避免Windows下换行符转换问题)
$file = fopen($filename, "rb");
// 读取文件的前2个字节(二进制数据)
$bin = fread($file, 2);
// 关闭文件句柄,释放资源
fclose($file);
// 解包二进制数据:C2chars表示将2字节数据转为2个无符号字符,存入$strInfo数组
// 结果类似:$strInfo = ['chars1' => 255, 'chars2' => 216](对应jpg文件头)
$strInfo = @unpack("C2chars", $bin);
// 将两个字节的数字拼接成整数,作为文件类型特征码
// 例如jpg:255(chars1)+216(chars2)→ 255216
$typeCode = intval($strInfo['chars1'].$strInfo['chars2']);
$fileType = '';
// 根据特征码匹配文件类型
switch($typeCode){
case 255216: // jpg文件头:FF D8 → 十进制255 216 → 拼接为255216
$fileType = 'jpg';
break;
case 13780: // png文件头:89 50 → 十进制137 80 → 拼接为13780
$fileType = 'png';
break;
case 7173: // gif文件头:47 49 → 十进制71 73 → 拼接为7173
$fileType = 'gif';
break;
default: // 非上述类型,返回unknown
$fileType = 'unknown';
}
return $fileType;
}
// 初始化状态变量:是否上传成功、错误提示
$is_upload = false;
$msg = null;
// 判断用户是否点击了上传按钮(提交了表单)
if(isset($_POST['submit'])){
// 获取上传文件的临时路径(PHP上传后会暂存到临时目录)
$temp_file = $_FILES['upload_file']['tmp_name'];
// 调用函数,校验文件真实类型
$file_type = getReailFileType($temp_file);
// 校验失败:文件类型不合法
if($file_type == 'unknown'){
$msg = "文件未知,上传失败!";
}else{
// 校验成功:拼接最终存储路径
// UPLOAD_PATH:固定的上传目录(如/upload)
// rand(10,99):2位随机数,避免文件名冲突
// date("YmdHis"):时间戳(年月日时分秒),进一步保证文件名唯一
// .$file_type:强制使用校验后的后缀(如.jpg/.png)
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$file_type;
// 将临时文件移动到最终路径,完成上传
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true; // 上传成功
} else {
$msg = "上传出错!"; // 上传失败(如权限不足、路径不存在)
}
}
}
这个案例会验证上传内容,确认是图⽚格式,因此不能再像以前一样将php强制转换为jpg格式。
这是正常的图片,

接下来我们制作木马图片,在物理机 cmd中执⾏以下命令:
bash
copy image.png /b + info.php /a webshell.png
如图,在划线位置输入cmd然后回车,执行命令后就会生成webshell.png文件

这就是含有木马的图片(记事本打开),将其上传即可。

因为上传的是一个图片,需要使用文件包含漏洞来把图片当作代码执行。

最终结果如图所示,

PASS-14(图**⽚⽊⻢**2)
PASS-14的源码如下:
javascript
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 = "上传出错!";
}
}
}
javascript
$info = getimagesize($filename);
getimagesize() 函数核心作用
getimagesize() 是 PHP 内置函数,专门用于获取图片文件的尺寸、类型、MIME 等信息 ,如果文件不是合法的图片(或损坏的图片),函数会返回 false。
它的核心能力:
- 读取文件的完整图片结构(不只是前 2 字节),校验文件是否符合图片格式规范;
- 返回图片的宽度、高度、类型(如
IMAGETYPE_JPEG)、MIME 类型(如image/jpeg)等信息; - 对损坏的图片、非图片文件会直接返回
false,比 "仅读前 2 字节" 更难被简单绕过。
虽然与PASS-13的源码不同,但是效果殊途同归,操作步骤同PASS-13。
PASS-15(图⽚⽊⻢3)
PASS-15的源码如下:
javascript
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() 是 PHP 中用于判断图像真实类型的内置函数,核心逻辑是读取文件 ** 首个字节的签名(文件头)** 并匹配,而非依赖文件名后缀,安全性优于单纯后缀校验,但仍存在局限性。
虽然与PASS-13的源码不同,但是效果殊途同归,操作步骤同PASS-13。