php函数及漏洞防御方式
一、文件上传核心函数1️⃣move_uploaded_file()bool move_uploaded_file(string $filename, string $destination)作用将 HTTP POST 上传的临时文件移动到目标路径考点先move()再检查 → Pass-17 / Pass-18 条件竞争save_path直接拼接 → Pass-11 / Pass-12 路径穿越目标名用户可控 → Pass-19 / Pass-20安全点PHP 5.3.4 受%00/\x00截断影响2️⃣$_FILES超全局数组$_FILES[upload_file][name] // 客户端原始文件名 $_FILES[upload_file][type] // MIME 类型客户端可控 $_FILES[upload_file][tmp_name]// 服务器临时文件路径 $_FILES[upload_file][size] // 文件大小 $_FILES[upload_file][error] // 错误码考点type完全来自浏览器 → Pass-20 MIME 白名单可绕靠数组name用户可控 → 几乎所有黑名单关的核心输入tmp_name才是可信的临时文件真实名二、黑名单 / 扩展名处理函数3️⃣strrchr()string strrchr(string $haystack, string $needle)作用查找字符串中最后一次出现 needle 的位置返回从该位置到结尾的字符串靶场用法$file_ext strrchr($file_name, .); // 取最后一个点之后的内容出现关卡Pass-05 ~ Pass-09漏洞点shell.php.→ 返回.空扩展名错觉和deldot()/trim()时序配合出问题4️⃣strtolower()string strtolower(string $str)作用字符串转全小写考点Pass-06源码有它黑名单大小写变体失效Pass-20数组绕过时先strtolower(explode)一句话考点黑名单防御神器没它必被大小写绕5️⃣str_ireplace()mixed str_ireplace(mixed $search, mixed $replace, mixed $subject)靶场两用法✅ 用法 A清::$DATAstr_ireplace(::$DATA, , $file_ext);Pass-09 专门防御 NTFS ADS✅ 用法 B黑名单单次替换漏洞str_ireplace($deny_ext, , $file_name);Pass-10 核心漏洞只替换一次 →shell.pphphp→shell.php 对比preg_replace()可递归str_ireplace()默认不递归6️⃣trim()string trim(string $str [, string $charlist ])作用去掉首尾空白字符空格、\t、\n、\r考点Pass-07先 trim 再取扩展名 →shell.php 被提前去空格时序问题先清再检 安全先检再清 漏洞7️⃣deldot()upload-labs 自定义函数function deldot($s){ /* 删末尾所有点 */ }作用从字符串末尾往前删.直到遇到非.考点Pass-08防御shell.php./shell.php..Pass-09配合::$DATA清理链结论专门防 Windows 后缀点绕过8️⃣in_array()bool in_array(mixed $needle, array $haystack [, bool $strict FALSE ])靶场用法in_array($file_ext, $deny_ext);坑点第三个参数默认FALSE→ 松散比较php 0之类弱类型不过靶场基本没踩这个坑Pass-13~20白名单检查也用它9️⃣pathinfo()mixed pathinfo(string $path [, int $options ])常用$ext pathinfo($file_name, PATHINFO_EXTENSION);Pass-19 漏洞点shell.php.→PATHINFO_EXTENSION 空字符串不在黑名单 → 过三、图片马 文件类型检测函数高频unpack()—— Pass-13array unpack(string $format, string $data)$bin fread($fp, 2); $strInfo unpack(C2chars, $bin);作用把二进制数据按格式拆包Pass-13 原理只取前 2 字节当 Magic Number考点文件头检测有多浅1️⃣1️⃣getimagesize()—— Pass-14array getimagesize(string $filename)返回[0]宽 [1]高 [2]类型常量 [3]html属性考点解析完整图片结构但不管文件尾附加数据搭配image_type_to_extension()1️⃣2️⃣image_type_to_extension()—— Pass-14string image_type_to_extension(int $imagetype [, bool $dot TRUE ])作用把IMAGETYPE_JPEG转成.jpeg考点保存名由函数决定所以扩展名变了1️⃣3️⃣exif_imagetype()—— Pass-15int exif_imagetype(string $filename)依赖php_exif.dll扩展考点比getimagesize()快只验签名不解析宽高仍不防图片马尾部不管1️⃣4️⃣ GD 库函数Pass-16 地狱函数作用imagecreatefromjpeg()JPEG 解码为内存图像imagecreatefrompng()PNG 解码imagecreatefromgif()GIF 解码imagejpeg()重新编码保存imagepng()PNG 重编码imagegif()GIF 重编码漏洞本质二次渲染 重新生成图片 → 普通图片马被清过关关键找 GD 渲染后不变的字节区域GIF 最稳四、路径 / 字符串 / 逻辑漏洞相关1️⃣5️⃣basename()文档常提靶场少用但重要string basename(string $path)防御写法提取纯文件名防../upload-labs 没用 → 所以才有 Pass-11/121️⃣6️⃣rand()/date()—— 重命名逻辑rand(10,99).date(YmdHis)考点随机重命名让攻击者猜不到路径但条件竞争时文件短暂存在Pass-17/181️⃣7️⃣reset()/end()—— Pass-20 灵魂函数reset($array); // 第一个元素 end($array); // 最后一个元素Pass-20 数组漏洞$file[0]shell.php $file[2]jpg end($file) jpg // 过白名单 reset($file)shell.php $file[1]NULL // 拼接出 shell.php.1️⃣8️⃣count()int count(array $array)Pass-20 坑稀疏数组count2但$file[1]不存在1️⃣9️⃣explode()array explode(string $delimiter, string $string)Pass-20$file explode(., strtolower($file));字符串变数组 → 和数组传参产生类型混淆五、文件包含图片马执行入口2️⃣0️⃣include/include_onceinclude $file;include.php 源码$file $_GET[file]; include $file;考点无视扩展名.jpg照样执行 PHP相对路径包含一句话图片马能过上传文件包含才是执行点2️⃣1️⃣file_exists()—— Pass-14 辅助bool file_exists(string $filename)只是判断文件存不存在没过滤2️⃣2️⃣unlink()bool unlink(string $filename)Pass-17先保存 → 不合法 →unlink()删除条件竞争窗口move_uploaded_file()到unlink()之间2️⃣3️⃣file_put_contents()—— 竞争验证file_put_contents(__DIR__./x.txt, ok);Pass-17 / 18 用来证明“代码真的执行了”六、PHP 特性 / 配置级函数 常量2️⃣4️⃣phpinfo()环境自查看short_open_tagasp_tagsLoaded Extensions → exif2️⃣5️⃣ 短标签相关Pass-16 有用写法是否可用?php ?✅? ?✅ PHP 5.4? ?❌% %❌2️⃣6️⃣ NULL 字节截断原理函数级底层move_uploaded_file()/fopen()调用 libcfopen()C 字符串遇\0终止 → PHP 5.3.4 前漏洞七、按「关卡 → 函数组合」速记表关卡核心函数组合一句话漏洞Pass-05in_array strtolower trim黑名单再全也有漏Pass-06strtolower strrchrWindows 大小写不敏感Pass-07trim(前) strrchr时序问题Pass-08deldot strtolower末尾点防御Pass-09str_ireplace(::$DATA)ADS 防御演示Pass-10str_ireplace(单次)pphphp双写Pass-11move_uploaded_file $_GET[save_path]GET %00 截断Pass-12move_uploaded_file $_POST[save_path]POST \x00 截断Pass-13fread unpack2字节文件头Pass-14getimagesize image_type_to_extension结构检测Pass-15exif_imagetype签名检测Pass-16imagecreatefromgif imagegifGD 二次渲染Pass-17move_uploaded_file unlink先存后删竞争Pass-18class move() renameFile()类内竞争Pass-19pathinfo $_POST[save_name]无清理可控名Pass-20explode reset end count 数组八、漏洞防御方式1. 前端 JS 校验Pass-01漏洞只在前端用 JS 拦后缀Burp 改包直接过。防御前端 JS 只做体验后端必须再校验一遍后端用白名单不依赖 JS// 后端硬校验 $allow [jpg,png,gif]; $ext strtolower(pathinfo($name, PATHINFO_EXTENSION)); if(!in_array($ext, $allow)) die(拒绝);2. MIMEContent-Type伪造Pass-02漏洞只信 $_FILES[type]客户端随便改。防御MIME 只辅助不信它必须配合扩展名白名单 文件内容检测扩展名白名单优先 $allow [jpg,jpeg,png,gif]; if(!in_array($ext, $allow)) die(拒绝);3. 扩展名黑名单不全Pass-03~05漏洞黑名单漏 .phtml、大小写、.htaccess、.user.ini防御用白名单不用黑名单扩展名先 strtolower() 再比Apache 禁止上传 .htaccessPHP 模式禁 .user.ini$allow [jpg,png,gif];4. 大小写绕过Pass-06漏洞没 strtolower()传 shell.Php防御取扩展名后先转小写再校验$ext strtolower(pathinfo($name, PATHINFO_EXTENSION));5. 空格 / 末尾点 / ::$DATAPass-07~09漏洞shell.php、shell.php.、shell.php::$DATA防御清理链顺序先 trim → deldot → 去 ::$DATA → 转小写 → 再白名单$name trim($name); $name deldot($name); // 去末尾点 $ext str_ireplace(::$DATA, , $name); // 去NTFS流 $ext strtolower(pathinfo($ext, PATHINFO_EXTENSION));6. 双写绕过Pass-10漏洞str_ireplace() 单次替换 → pphphp 变 php防御白名单根本不怕双写非要替换就循环替换到不出为止但正解还是白名单// 正解白名单不玩替换游戏 if(!in_array($ext, $allow)) die(拒绝);7. %00 / \x00 路径截断Pass-11~12漏洞save_path../upload/shell.php%00 截断防御PHP ≥ 5.3.4 已修但代码层还要防save_path 不许用户传服务端自己定用 basename() 取纯文件名$save_path /var/www/upload/; // 写死 $name basename($tmp_name); // 不让用户控路径 $final $save_path . uniqid() . .jpg;8. 图片头 / getimagesize / exif 绕过Pass-13~15漏洞图片马头部合法尾部藏 PHP靠 include 执行防御三层扩展名白名单getimagesize() 或 exif_imagetype() 验结构图片二次渲染GD 重生成上传目录禁止脚本执行 include 用白名单$info getimagesize($tmp); if(!in_array($info[2], [IMAGETYPE_GIF,IMAGETYPE_JPEG,IMAGETYPE_PNG])) die(拒绝); // 二次渲染 $img imagecreatefromjpeg($tmp); imagejpeg($img, $final);9. GD 二次渲染绕过Pass-16漏洞找稳定区域塞 PHP 代码防御用 GD 重新生成新文件不存原图新文件用随机名原图上传后立刻删imagejpeg(imagecreatefromgif($tmp), $final); unlink($tmp);10. 条件竞争Pass-17~18漏洞先 move_uploaded_file 再删中间有窗口防御先检查后保存不在 Web 目录先存临时处或临时目录不在 Web 根验证过再拷过来上传目录禁执行// 先放临时目录外网访问不到 $tmp /tmp/upload_.uniqid(); move_uploaded_file($_FILES[f][tmp_name], $tmp); // 验扩展名内容 if(ok($tmp)){ rename($tmp, $real_path); }else{ unlink($tmp); }11. save_name 用户可控Pass-19漏洞POST 控文件名绕大小写/点/::$DATA防御不用用户给的文件名服务端 uniqid()..jpg 自己拼$final UPLOAD_PATH./.uniqid()..jpg;12. 数组参数逻辑漏洞Pass-20漏洞save_name[0]shell.phpsave_name[2]jpg 骗白名单防御save_name 当字符串处理强制转型或根本不收这个参数$name is_array($_POST[save_name]) ? : $_POST[save_name]; $name basename($name);13. 文件包含执行图片马include.php 通病漏洞include($_GET[file]) 无视扩展名防御包含路径白名单 / 不允许用户传 path上传目录Apache/Nginx 配成不解析nginx location /upload/ { php_flag engine off; # Apache # Nginx: 不套 fastcgi_pass }通用“铁桶防御”模板1. 扩展名白名单jpg/png/gif2. strtolower trim deldot 去::$DATA3. 不用原文件名 → uniqid() 重命名4. save_path 写死basename 提取5. 图片类getimagesize GD 二次渲染6. 上传目录禁脚本执行Apache/Nginx 配7. 先校验后保存临时目录不在 Web 根8. include 不接受用户路径