背景
在雷池社区版 (https://github.com/chaitin/safeline) 发布的第一天,weaweawe01师傅就发现了雷池的php代码注入检测模块没有支持php8的新语法的问题 (https://github.com/chaitin/safeline/issues/1) 。对此,我们进行了紧急的修复,之后支持了所有php8的新语法、新特性。
这次的修复主要是结合php官方手册介绍和zend引擎代码进行修复,同时兼容了一些php5、php7的语法内容。
绕过分析
雷池社区版发布的第一天,weaweawe01 师傅给我们提了一个 issue,他的文件上传绕过了雷池检测,使用的是php8语法。
让我们看看他是怎么绕过的。
<?php
class Money
{
public function __construct(
public Currency $currency,
public int $amount
) {}
}
phpinfo();
?>
放在php7的环境上试了一下,发现报错了,语法错误,看起来是public后希望是变量而不是类型名
由于雷池使用的语义分析引擎,并不是规则引擎。我们需要得到正确的语义分析结果才会判断攻击特征。
在引擎调试了一下,词法分析是没有问题的,能够正确的分词。但是在语法分析的时候出现了错误,代表着它不符合引擎内置的php语法。看来需要进行升级了!
雷池语义分析引擎已经在2023年5月支持php8语法特性~
于是,在 php官方手册查找,发现是php8的新特性-构造器属性提升,它让构造器的参数也可以相应提升为类的属性,这个新特性在引擎的语法分析还没有支持。为此,我们重新梳理了php8的语法,对php8的语法进行了支持,同时,也发现了很多有趣的新特性。
新特性
构造器属性提升
PHP 8.0.0 起,构造器的参数也可以相应提升为类的属性。这个特性对于一些需要在构造器中设置或初始化一些类属性的时候非常有用(包括public
、protected
和private
)
构造器属性提升语法
在php7里需要先声明参数类型,再传参
<?php
class Money
{
public Currency $currency;
public int $amount;
public function __construct(
Currency $currency,
int $amount,
) {
$this->currency = $currency;
$this->amount = $amount;
}
}
?>
现在在php8里可以直接这样写
<?php
class Money
{
public function __construct(
public Currency $currency,
public int $amount,
) {}
}
?>
命名参数
php8开始引入命名参数,允许按照参数名传参,命名参数通过在值前边添加参数名和冒号来传递,传参的顺序与定义的顺序无关,参数名只是个标识符,不允许动态指定。
命名参数语法
<?php
//支持
function_a(parameter_a: $value_a, parameter_b: $value_b)
//不支持
function_a($parameter_a: $value_a)
?>
联合类型
联合类型可以接收多种不同类型的值,不只是单一的类型,更加的灵活。在php8之前是没有直接支持联合类型的,需要通过phpdoc来声明。
联合类型语法
<?php
function sum(int|float $a, int|float $b): int|float
{
return $a + $b;
}
sum(1, 1.1) // return 2.1
?>
sum
函数可以接受int、float类型的参数,更加的灵活。
match 表达式
match
表达式基于值的一致性进行分支计算。match
表达式和switch
语句类似,都有一个表达式主体,可以和多个可选项进行比较。与switch
不同点是,它会像三元表达式一样求值。另一个不同点,它的比较是严格比较===
而不是松散比较==
。
match 表达式语法
<?php
$food = 'cake';
$return_value = match ($food) {
'apple' => 'This food is an apple',
'bar' => 'This food is a bar',
'cake' => 'This food is a cake',
};
var_dump($return_value); // string(19) "This food is a cake"
?>
Nullsafe 运算符
自PHP 8.0.0起,类属性和方法可以通过"Nullsafe"操作符访问:?->
。除了一处不同,Nullsafe操作符和以上原来的属性、方法访问是一致的:对象引用解析(dereference)为null时不抛出异常,而是返回null。并且如果是链式调用中的一部分,剩余链条会直接跳过。
此操作的结果,类似于在每次访问前使用is_null()
函数判断方法和属性是否存在,但更加简洁。
Nullsafe 运算符语法
<?php
$result = $repository?->getUser(5)?->name;
// 上边那行代码等价于以下代码
if (is_null($repository)) {
$result = null;
} else {
$user = $repository->getUser(5);
if (is_null($user)) {
$result = null;
} else {
$result = $user->name;
}
}
?>
仅当 null 被认为是属性或方法返回的有效和预期的可能值时,才推荐使用 Nullsafe
操作符。如果业务中需要明确指示错误,抛出异常会是更好的处理方式。
枚举
enum
类似class
,它和class
、interface
、trait
共享同样的命名空间。也能用同样的方式自动加载。一个enum
定义了一种新的类型,它有固定、数量有限、可能的合法值。
枚举语法
<?php
enum Suit
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
?>
以上声明了新的枚举类型Suit
,仅有四个有效的值:Suit::Hearts
、Suit::Diamonds
、Suit::Clubs
、Suit::Spades
。变量可以赋值为以上有效值里的其中一个。函数可以检测枚举类型,这种情况下只能传入类型的值。
<?php
function pick_a_card(Suit $suit) { ... }
$val = Suit::Diamonds;
pick_a_card($val); // OK
pick_a_card(Suit::Clubs);// OK
pick_a_card('Spades'); // TypeError: pick_a_card(): Argument #1 ($suit) must be of type Suit, string given
?>
交集类型
交集类型接受满足多个类类型声明的值,而不是单个值。交集类型中的每个类型由&
符号连接。因此,类型T
、U
和V
组成的交集类型将写成T&U&V
。
注意:交集类型不能与联合类型一起使用。
readonly属性
自 PHP8.1.0 起,可以使用readonly
修饰符声明属性,防止初始化后修改属性。
readonly属性语法
<?php
class Test {
public readonly string $prop;
public function __construct(string $prop) {
// 初始化正常。
$this->prop = $prop;
}
}
$test = new Test("foobar");
// 读取正常。
var_dump($test->prop); // string(6) "foobar"
// 再赋值异常。分配的值是否相同并不重要。
$test->prop = "foobar"; // Error: Cannot modify readonly property Test::$prop
?>
更多php新特性可以访问php官方手册介绍,可以白嫖commit哦!
链接
-
• weaweawe01: https://github.com/weaweawe01
-
• php官方手册介绍: https://www.php.net/manual/zh/index.php
-
• zend引擎: https://github.com/php/php-src/tree/master/Zend
-
• 构造器属性提升: https://www.php.net/manual/zh/language.oop5.decon.php#language.oop5.decon.constructor.promotion
结束语
欢迎感兴趣的师傅进群讨论~
注:如果二维码过期,可在公众号回复“加群”获取最新进群二维码
原文始发于微信公众号(Skynet 安全团队):记一次php语法绕过引发的php8语法特性追踪
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论