PHP中的一些语法特性

 CTF / Web
被浏览

整理了一些 PHP 中的一些常在 CTF 比赛中用到的语法特性,也算是自用笔记。

短标签

常见的 PHP 代码都被类似 <?php ...... ?>,但也支持缩写形式 <?= ...... ?>,在 short_open_tag 开启的时候也可以使用 <? ...... ?>

其中 <?= 是更完整的 <?php echo 的简写形式。

而且单文件时右边可以不闭合,形如 <?php ......,其他两个同理。

应用场景:文件上传或者文件包含时绕过 WAF 或者过滤。

因为考虑到和 XML 格式的兼容,默认情况下 short_open_tag 是被关闭的。

伪协议

URL 风格的封装协议,可用于类似 fopen()、 copy()、 file_exists() 和 filesize() 等文件系统函数。除了这些封装协议,还能通过 stream_wrapper_register() 来注册自定义的封装协议。

具体启用了哪些可以在 php_info 中的 Registered PHP Streams 查看。

应用场景:常和 include file_get_contents 等函数配合使用,实现文件包含。

file://

用来访问本地文件,当指定了一个相对路径(不以 /、\、\\ 或 Windows 盘符开头的路径)提供的路径将基于当前的工作目录。在很多情况下是脚本所在的目录,除非被修改了。

应用场景:读取 flag 或者是某些敏感文件。

http://

常规 URL 形式,允许通过 HTTP 1.0 的 GET方法,以只读访问文件或资源。

应用场景:通常用于远程包含。

php://

用来访问各个输入/输出流。

php://input

php://input 是个可以访问请求的原始数据的只读流。enctype="multipart/form-data" 的时候 php://input 是无效的。

会读取 POST DATA 的内容。

php://filter

php://filter 是一种元封装器,设计用于数据流打开时的筛选过滤应用。这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()file()file_get_contents(),在数据流内容读取之前没有机会应用其他过滤器。

php://filter 参数描述
resource=<要过滤的数据流>这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表>该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
write=<写链的筛选列表>该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
<;两个链的筛选列表>任何没有以 read= 或 write= 作前缀 的筛选器列表会视情况应用于读或写链。

完整的可用的过滤器列表,下面只列举一些常用的过滤器。

字符串过滤器作用
string.rot13等同于str_rot13(),rot13变换
string.toupper等同于strtoupper(),转大写字母
string.tolower等同于strtolower(),转小写字母

转换过滤器作用
convert.base64-encode等同于 base64_encode(),base64编码
convert.base64-decode等同于 base64_decode(),base64解码

例子:php://filter/read=convert.base64-encode/resource=${file}

php://input 受限于 allow_url_include

zlib://

gz 格式文件的解压伪协议,似乎是 PHP 自带实现的?

例子: compress.zlib://${file.gz}

zip://

zip 格式文件的解压伪协议,由 ZIP 扩展注册。

例子:zip://${archive.zip}#${dir/file},在实际应用中 # 常用 URL 编码 %23 来代替。

如果不存在 ZIP 扩展,则不存在该伪协议。

data://

data:// — 数据(RFC 2397)

根据 RFC 文档的规定,一个 data 流格式应如下:

1
2
3
4
dataurl    := "data:" [ mediatype ] [ ";base64" ] "," data
mediatype := [ type "/" subtype ] *( ";" parameter )
data := *urlchar
parameter := attribute "=" value

例子:data://text/plain;base64,ZGF0YQ==

glob://

glob:// — 查找匹配的文件路径模式。

glob 模式使用通配符来广泛的匹配文件名,更加详细的介绍可以看 Wiki

例子:glob://f[k-m]*,用来匹配以字符串 fl 开头的文件。

phar://

phar:// 用于处理 Phar 数据流,支持多种压缩方式。

具体启用了哪些压缩方式可以通过 php_info 找到 Phar 模块,表格中有 ${type}-based phar archives,其中 type 即为压缩方式。

例子:phar://${archive.zip}/${file}

关于 Phar 还有更多,我会在新的文章里讲到它。

PHAR (PHp ARchive) 是 PHP 里类似于 JAR (Java ARchive) 的一种打包文件,通常后缀名为 .phar。如果你使用的是 PHP 5.3 或更高版本,那么 Phar 后缀文件是默认开启支持的,你不需要任何其他的安装就可以使用它。

序列化与反序列化

详见这篇文章

include 及类似函数

1、include() 当使用该函数包含文件时,只有代码执行到 include() 函数时才将文件包含进来,发生错误时只给出一个警告,继续向下执行。

2、require() 只要程序一执行就会立即调用文件,发生错误的时候会输出错误信息,并且终止脚本的运行。

include_once()、require_once() 功能和 include()、require() 相同,但它们把要加载的文件存放在一个哈希表中,再次加载时检查这个哈希表,如果发现已经加载过则直接跳过。

被包含的文件将继承引入前具有的全部变量范围,比如调用文件前面定义了一些变量,那么这些变量就能够在被包含的文件中使用,反之,被包含文件中定义的变量也将从调用处开始可以被被调用文件所使用。

被包含文件中定义的函数、类在执行之后将可以被随处使用,即具有全局作用域。

有趣的是,如果被引入的文件与源文件变量重名,将会覆盖原有的文件中的变量。

举个例子,现在有两个文件 a.phpb.php

1
2
3
4
5
6
7
8
9
10
// a.php
<?php
class Fl4g {
function __toString() {
return "NoNoNo";
}
}
$x = new Fl4g();
include("b.php");
echo $x;
1
2
3
4
5
6
7
8
// b.php
<?php
class Flag {
function __toString() {
return "YesYesYes";
}
}
$x = new Flag();

运行 a.php 会输出 YesYesYes,但值得注意的是并不能覆盖定义原有的类,在刚才的例子中类的名字分别为 Fl4g 和 Flag,重名将会报错。

参考链接:https://www.kancloud.cn/nickbai/php7/content/4.5include-require.md

allow_url_include 参数开启的情况下允许解析 PHP 伪协议。

__halt_compiler

用于中断编译器的执行,在 Phar 中则以 __HALT_COMPILER(); 来结尾。

应用场景:发现无法闭合右标签且后面会导致编译错误时可用。

弱类型比较(==)

强类型,就是强制数据类型定义。也就是说,一旦变量被指定了某个数据类型,如果不经过强制类型转换,那么它永远都是这个数据类型。

弱类型,对数据类型要求不严格,可以让数据类型随意互相转换,利用这些特性,我们可以对一些条件进行绕过。

放一段大佬的 fuzz 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
function fuzz() {
$a = [true, false, 1, 0, -1, "1", "0", "-1", NULL, array(), "Array", "null", "true", "false", "", 0e123, "0e123", "0e0", 0e0, 9e999, INF, "9e999", "INF", -9e99, "-0e0", -0e0];
for ($i = 0; $i < sizeof($a); $i ++) {
$t = (string)$a[$i];
for ($j = 0; $j < sizeof($a); $j ++) {
if($t !== $a[$j] && md5($t) === md5($a[$j]) && $t != $a[$j]) {
// 这里可以写一些题目里的条件,在这里实际上输出全是 INF
echo "--------\n";
echo "pos:".$i."\n";
var_dump($a[$i]);
echo "pos:".$j."\n";
var_dump($a[$j]);
}
}
}
}
fuzz();

值得一提的是,switch 也是弱类型比较的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$a = '1e0';
switch ($a) {
case 1:
echo "1";
break;
case 2:
echo "2";
break;
default:
echo "default";
break;
}
// output: 1

字符串和数字比较

在 PHP 中,一个字符串可能被解析成两种类型:普通的字符串或者是数字字符串,值得注意的是,在 PHP8.x 中只有形如 <str1>e<str2> 的字符串只有 <str1><str2> 全为数字时才会被解析成数字字符串,而 PHP5.x 以及 PHP7.x 中只要以数字开头都可。

1
2
3
4
5
<?php
var_dump("1e5" == 100000); // bool(true)
var_dump("2a" == 2);
// PHP5.x PHP7.x => bool(true)
// PHP8.x => bool(false)

普通字符串和数字比较,字符串会被转换成数字 $0$。
该特性只在 PHP 5.x 和 PHP 7.x 上生效。

举个例子:

1
2
<?php
var_dump("str" == 0); // bool(true)

以下示例来自官方文档,可以看到存在精度误差。

1
2
3
4
5
6
7
8
$foo = 1 + "10.5";                // $foo 是 float (11.5)
$foo = 1 + "-1.3e3"; // $foo 是 float (-1299)
$foo = 1 + "bob-1.3e3"; // PHP 8.0.0 起产生 TypeError;在此之前 $foo 是 integer (1)
$foo = 1 + "bob3"; // PHP 8.0.0 起产生 TypeError;在此之前 $foo 是 integer (1)
$foo = 1 + "10 Small Pigs"; // PHP 8.0.0 起,$foo 是 integer (11),并且产生 E_WARNING;在此之前产生 E_NOTICE
$foo = 4 + "10.2 Little Piggies"; // PHP 8.0.0 起,$foo 是 float (14.2),并且产生 E_WARNING;在此之前产生 E_NOTICE
$foo = "10.0 pigs " + 1; // PHP 8.0.0 起,$foo 是 float (11),并且产生 E_WARNING;在此之前产生 E_NOTICE
$foo = "10.0 pigs " + 1.0; // PHP 8.0.0 起,$foo 是 float (11),并且产生 E_WARNING;在此之前产生 E_NOTICE

特殊的,在 PHP 5.x,“0x” 开头的字符串能被解析成 16 进制的数字。

1
2
3
4
var_dump(0x123);          // int(291)
var_dump((int)'0x123'); // int(0)
var_dump('0x123' == 0x123); // bool(true)
var_dump('0x123' == 291); // bool(true)

但是显而易见的,这种解析方法存在歧义,比如第二个也可以被解析成 0,所以才会被弃用的吧。

字符串和字符串比较

字符串之间的比较有个很有意思的点,那就是如果字符串本身如果不相等的话,解释器会尝试把它们转换为数字字符串再进行比较。

举个例子:

1
2
3
4
var_dump("1e5" == "100000");      // bool(true)
var_dump("1e5" == "10e4"); // bool(true)
var_dump("1e5" == "1e5a"); // bool(false)
var_dump("0e12345" == "0e67890"); // bool(true)

利用这一点,我们可以让一些字符串函数的返回值以 0e 开头来搞点事情,详细的放在了[函数篇中的字符串函数]中。

bool 类型和字符串比较

bool 类型的 true 可以和任意非空、解析后非零字符串相等,false 则反之。

举个例子:

1
2
3
4
var_dump('0' == true);  // bool(false)
var_dump('' == true); // bool(false)
var_dump('0a' == true); // bool(true)
var_dump('a' == true); // bool(true)

bool 类型和数字比较

bool 类型的 true 可以和任意非零数字相等,反之亦然。

举个例子:

1
2
3
4
var_dump(0 == true);       // bool(false)
var_dump(1 == true); // bool(true)
var_dump(1.0 == true); // bool(true)
var_dump((1 - 1) == true); // bool(false)