关于oop:PHP奇怪:绕过类可见性?

PHP Oddities: Bypass Class Visibility?

今天我在一个旧的诊断库工作,我在5.1天的宁静中写了回来。该库提供了您可以给它的任何变量的高度详细的转储,使用颜色编码来指示类型,并使用反射来产生对对象的许多洞察(包括,根据您传递的标志,为对象输出相关的phpdoc,甚至是源代码-特别是在回溯中有用)。

当时,我可以绕过成员可见性来输出类的受保护成员和私有成员的值。从调试的角度来看,这是非常有用的,特别是在我们生成的详细错误日志方面。

为了绕过5.1中的可见性,我使用了反射API,它让您看到ReflectionMethod->getValue($object)中受保护成员和私有成员的价值。当然,这有点绕过了安全性,但也不算太糟糕,因为如果您要以这种方式查看和修改值,很明显会破坏对象的预期API。

php 5.2阻止反射访问受保护/私有成员和方法。当然,这是故意的,并且认为这种能力是一个安全问题。我只是在我的库中添加了一个TIG/catch,如果语言允许的话,我就把它输出,如果没有的话,Java反射AFAICR总是允许你绕过可见性(我相信他们认为,如果你想要它足够严重的话,你会以某种方式得到它,可见性只是一个对象的广告API,vi。这是你自己的风险)。

作为一个思想练习,也许是为了更新我的dump库,我很好奇是否有人能想出巧妙的方法来绕过现代版本的php(5.2+)中的可见性,但我特别感兴趣的是php 5.3。

有三条路看起来特别有希望。第一:损坏序列化/非序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
Class Foo {
    protected $bar;
    private $baz;
}
class VisibleFoo {
    public $bar;
    public $baz;
}

$f = new Foo();
$data = serialize($f);
$visibleData = str_replace($data, 'O:3:"Foo":', 'O:10:"VisibleFoo":');
$muahaha = unserialize($visibleData);

当然,这比这更为复杂,因为受保护的成员被标记为:null*null属性,私有成员被绑定到其原始类名:nulloriginalclassnull属性(参见php序列化),但理论上,您可以清除所有这些内容并将其序列化/非序列化转换为e为您分解这些值。

这有几个缺点:首先,它在语言版本方面是脆弱的。php不能保证serialize()生成的数据在不同版本之间保持一致(事实上,自从我使用php以来,保护成员和私有成员的表示方式已经发生了变化)。第二,更重要的是,一些对象声明了一个uu sleep()方法,这可能会产生意想不到的副作用:1)不允许您访问所有私有成员;2)这可能会破坏数据库连接、关闭文件流,或者认为实际上不会睡觉的对象的其他副作用。

第二种选择是尝试解析print_r()或其他内置调试语句来获取值。这样做的结果是,除了简单的值之外,要做得更好是非常困难的(我的旧库会让您深入到自己是对象的成员中,等等)。有趣的是,它是这种方法的一种变体,我使用var_dump()检测无限递归($a->b = &$a)。

第三种选择是对目标进行子类化,并以此方式提高其可见性。这将使您可以访问受保护的成员,但不能访问私人成员。

我似乎记得几年前,有人读过一篇文章,他想出了一个绕过lambda函数的方法,或是其他类似的方法。我再也找不到了,我试过各种各样的方法来改变这个想法,但结果都是空的。

TLDR版本:有没有人想到要通过一些神奇的圈套来挖掘PHP对象实例的受保护和私有成员?


第四种选择:

1
$reflectionProperty->setAccessible(true);

可以使用getValue()方法使任何属性都可以访问,即使它是受保护的或私有的。测试可见性,然后使用setaccessible(true)、getvalue()和setaccessible(false)重置。

我认为,将serialize()/unserialize()转换为具有所有公共属性的新类会更干净。而且不要求你有所有类的重复版本


正如对任何遇到此主题的人都要注意的一点一样,寻找一种方法让调试脚本或其他任何东西打开受保护的属性,以便在内部进行探测,可能最简单和最深远的方法是绕过调试脚本的适当的include/require系统,并加载使用eval()调试的代码,就像在这个快速示例中一样。充足的:

1
2
3
4
$code = file_get_contents('ClassFile.php');
$code = trim($code, '<?php>');
$code = str_replace('protected', 'public', $code);
eval($code);

当然,您也可能希望用public替换private关键字,并且最好更小心地替换代码,以避免将字符串"protected"替换为实际上不是protected关键字的任何地方,等等。

我敢肯定,我不需要说,在任何一种常规的基础上或出于开发以外的任何原因进行这项工作都离最佳实践很远。但是,如果不删除eval()函数,这种绕过受保护/私有成员的方法不太可能很快被php的更新所破坏。