ambionics / phpggc

PHPGGC is a library of PHP unserialize() payloads along with a tool to generate them, from command line or programmatically.
https://ambionics.io/blog
Apache License 2.0
3.2k stars 492 forks source link

Adding Symfony 1.0 to 1.5 RCE gadget chains #182

Closed darkpills closed 6 months ago

darkpills commented 7 months ago

I created 2 gadget chains for all versions of Symfony 1 including the 1.5 fork maintained here https://github.com/FriendsOfSymfony1/symfony1

Symfony1 is not maintained any more. Sensiolabs has been contacted but they won't provide a fix since Symfony1 is end of life.

Symfony1.5.13 and greater are still partially vulnerable depending on how you install it, but no maintainers are responding.

cfreal commented 7 months ago

Hello,

Thanks! I like the usage of process_serialized(). Can you tell me how you created your test environment, because although the library is present, Swift classes don't seem to be included by default. I tried downloading the instance from Symfony's website, and using composer.

Charles

darkpills commented 7 months ago

Hello !

Thank you for your tests!

Current version of Symfony 1.5 is partially vulnerable. All depend how you install it:

darkpills commented 7 months ago

Here are my notes if it can help about installing 1.5:

With composer

Install composer:

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === 'e21205b207c3ff031906575712edab6f13eb0b361f2085f1f1237b7126d785e826a450292b6cfd1d64d92e6563bbde02') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

And then install Symfony with composer:

php composer.phar require friendsofsymfony1/symfony1 "1.5.*"
php composer.phar install

If you launch install command like in 1.4 you will get a fatal error due to Doctrine components not found. You can either add Doctrine with composer and declare sf_doctrine_path in your project configuration file, or like me since we don't need an ORM, disable Doctrine during installation. Edit vendor/friendsofsymfony1/symfony1/lib/plugins/sfDoctrinePlugin/config/installer.php:

$this->installDir(dirname(__FILE__).'/skeleton');
//$this->enablePlugin('sfDoctrinePlugin');
$this->reloadTasks();

And start creating a project, an application and a module:

php ./vendor/symfony/data/bin/symfony generate:project test
php symfony generate:app frontend
php symfony generate:module frontend test

Several files will be created, among them: web/index.php and web/frontend_dev.php for respectively production and development configuration of the frontend application.

Then start a webserver with PHP installed and make the root point to web directory (renamed public in version 2.x and higher).

Fix the possible permission issues.

Open a web browser to http://localhost/frontend_dev.php.

With git

Create a git repo:

git init .
git submodule add https://github.com/FriendsOfSymfony1/symfony1.git lib/vendor/symfony
git submodule update --init --recursive

If you try to generate a project, you will get an error due to PHP7.1+ new syntax to declare visibility of constants. However, this syntax is not compatible with PHP5. Here is a snipet to fix it:

find . -name "*.php" -type f -exec grep -l "public const" '{}' \; | noglob xargs -I '{}' sed -i -e s#"public const"#"const"# '{}'

Then, the rest is the same:

php ./vendor/bin/symfony generate:project test
php symfony generate:app frontend
php symfony generate:module frontend test
cfreal commented 7 months ago

Hello,

Symfony/RCE12

Thanks for the detailled information. I was able to pull it off with the GIT method.

Symfony/RCE13

Despite a successful installation, when I unserialized, I get the dreaded __PHP_Incomplete_Class for every lime_* object. The lime classes you use in the gadget are NOT in scope when I run the exploit from the symfony website, because lime.php is never included in the web part of the framework, or present in the autoloader.

For the sake of precision: I installed everything using the official method described here. I then put the unserialize() code in sfContext::getController() to make sure that everything has been loaded. I get (stripped for brevity):

object(__PHP_Incomplete_Class)#26 (10) {
  ["__PHP_Incomplete_Class_Name"]=>
  string(9) "lime_test"
  ["output":protected]=>
  object(__PHP_Incomplete_Class)#27 (2) {
    ["__PHP_Incomplete_Class_Name"]=>
    string(17) "lime_output_color"
    ["colorizer"]=>
    object(sfOutputEscaperObjectDecorator)#28 (2) {
      ["value":protected]=>
      object(__PHP_Incomplete_Class)#29 (1) {
        ["__PHP_Incomplete_Class_Name"]=>
        string(14) "lime_colorizer"
      }
      ...
    }
  }
  ["output"]=>
  object(__PHP_Incomplete_Class)#31 (2) {
    ["__PHP_Incomplete_Class_Name"]=>
    string(17) "lime_output_color"
    ...
 ...
}

Charles

darkpills commented 7 months ago

Thank you a lot for the time you spent on testing :)

Actually, there are a lot of possibilities for the chains, but i tried to provide as less chains as possible (2) to bring all the possible coverage on all versions of symfony 1.

The first chain (Symfony/RCE12) covers versions 1.3.0 < 1.5.13~17 including 1.4.x and with a "maybe" between 1.5.13 and 1.5.17 depending on how you install it.

The second chain (Symfony/RCE13) covers versions 1.0.0 < 1.2.12. For those versions, lime_test is loaded in the autoload of symfony, as seen in this extract of www-1.2.12/cache/frontend/dev/config/config_autoload.yml.php:

'sfPager' => '/var/www/lib/addon/sfPager.class.php',
  'lime_test' => '/var/www/lib/vendor/lime/lime.php',
  'lime_output' => '/var/www/lib/vendor/lime/lime.php',
  'lime_output_color' => '/var/www/lib/vendor/lime/lime.php',
  'lime_colorizer' => '/var/www/lib/vendor/lime/lime.php',
  'lime_harness' => '/var/www/lib/vendor/lime/lime.php',
  'lime_coverage' => '/var/www/lib/vendor/lime/lime.php',
  'lime_registration' => '/var/www/lib/vendor/lime/lime.php',
  'sfFunctionCache' => '/var/www/lib/cache/sfFunctionCache.class.php',

However, the installation of version 1.x is a bit tricky. I have a blog post waiting to be published. You can find above an extract of the installation notes if it can help:

Version 1.4

Install documentation is still available at https://symfony.com/legacy/doc/getting-started/1_4/en/03-Symfony-Installation and need few changes to get it running:

As the SVN infrastructure has been discontinued, download the version from git and untar the project:

tar -xvzf symfony1-*.tar.gz
mv symfony1-*/* .

And start creating a project, an application and a module:

php ./lib/vendor/symfony/data/bin/symfony generate:project test
php symfony generate:app frontend
php symfony generate:module frontend test

Version 1.3

Same as 1.4, except a light change in default symfony bin path:

php ./data/bin/symfony generate:project test

Version 1.2

In version 1.2 and lower, full installation path can be found in cache files and project configuration class like config/ProjectConfiguration.class.php:

require_once '/workspace/www-1.2.12/lib/autoload/sfCoreAutoload.class.php';
sfCoreAutoload::register();

class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    // for compatibility / remove and enable only the plugins you want
    $this->enableAllPluginsExcept(array('sfDoctrinePlugin', 'sfCompat10Plugin'));
  }
}

So depending on your setup, you might need to play with symlinks on your host/docker.

In lower versions of symfony, deprecated warnings can be seen. Error reporting verbosity can be changed in apps/frontend/config/settings.yml.

Version 1.1

Same as 1.2

Version 1.0

Same as 1.2, except some changes in the initial task names and no default symlink created:

php ./data/bin/symfony init-project test
php ./data/bin/symfony init-app frontend
php ./data/bin/symfony init-module frontend testclone

In 1.0 versions, the actions file name as been renamed:

Also, the request object is not given as an argument to the controller, and it needs to be fetch manually from sfContext singleton by the developper:

class testActions extends sfActions
{
  public function executeIndex()
  {
    $request = sfContext::getInstance()->getRequest();
    $a = unserialize($request->getParameter('user'));
  }
}
cfreal commented 7 months ago

We seem to have a different autoload configuration; I don't have lime.php in cache/frontend/dev/config/config_autoload.yml.php, and you do.

$ cat cache/frontend/dev/config/config_autoload.yml.php | grep lime -i
$

PS: Also, your folder name is www-1.2.12. Is it vulnerable as well ? You indicate that it is not (using < instead of <=).

darkpills commented 7 months ago

Could you precise the version of PHP you are using so that i can try to reproduce? I'm on PHP 5.6.40 for testing sf <= 1.4 to match with configuration of most applications which would remain on a 5.x stack.

I will need to update the versions. It's a <= for all the mentioned versions.

cfreal commented 7 months ago

Yes, sorry: PHP 5.6.40-68+ubuntu22.04.1+deb.sury.org+1 (cli)

darkpills commented 7 months ago

OK, this is my fault: I uncompressed the symfony archive in the current directory, not in lib/vendor, making all symfony classes being autoloaded, even the one in lib/vendor/symfony/vendor/*

Arf, it breaks my chain for symfony <= 1.2.12 with a __destruct(). I will make a commit to remove this last one and propose other alternative chains, maybe based on Propel.

thePanz commented 7 months ago

@darkpills any updates on this?

ps: would you please check the advisory message thread? we made some progress and we're waiting for your advice. thank you

darkpills commented 7 months ago

@thePanz the chain for >= 1.3.0 is valid, but the other one is not. I need to put more work into this one to provide complete chains. This can be done within the next days I think.

cfreal commented 7 months ago

OK, this is my fault: I uncompressed the symfony archive in the current directory, not in lib/vendor, making all symfony classes being autoloaded, even the one in lib/vendor/symfony/vendor/*

Oh, makes sense.

darkpills commented 7 months ago

@cfreal : i provided the following updates and new chains replacing the old one to provide a coverage on all versions. However, most of these depends on enabled plugins ORM.

However^2, I found another nice gadget chain covering almost all versions of 1.x and without any dependencies. I will publish it here later on. I opened another advisory on Symfony1 repository and wait for them to patch it.

Hope this time, there won't be any mistake :)

Another question for you that is not directly linked with the following PR: a class in the framework has this code:

public function __destruct()
{
    @mysql_close($this->db);
}

I dig in the php5 mysql extension source code to find any "callback" from C code to PHP code to trigger a gadget chain. However, I could only find zend resource access by with a zval input and resource deletion. Do you have the same analysis? Just if you have time to check, if not don't bother :)

cfreal commented 7 months ago

Hello,

Now PEAR is giving me hell. I cannot install the modules:

php5.6 -dinclude_path=/usr/local/lib/php:. ./symfony plugin:install sfPropelPlugin
>> plugin    installing plugin "sfPropelPlugin"
>> sfPearFrontendPlugin Attempting to discover channel "pear.symfony-project.com"...
>> sfPearFrontendPlugin Attempting fallback to https instead of http on channel
>> sfPearFrontendPlugin "pear.symfony-project.com"...

  Unable to register channel "pear.symfony-project.com" (use --force-license to force installation)

Same as root, same with --force-license.

Can you walk me through your steps?

Charles

darkpills commented 6 months ago

Actually, sfDoctrinePlugin and sfPropelPlugin are already bundled with the tar.gz of Symfony distribution from the legacy website. I did not install any third party component.

Here the is steps for 1.2:

mkdir lib/vendor 
cd lib/vendor
tar -xvzf ../../symfony1-*.tar.gz
mv symfony1-* symfony

php ./lib/vendor/symfony/data/bin/symfony generate:project test
php symfony generate:app frontend
php symfony generate:module frontend test

chmod 777 cache/ log/ 

sed -i -e s#"die("#"//die("#g ./web/frontend_dev.php
 sed -i -e s#"\$this->forward('default', 'module');"#"\$a = unserialize(\$request->getParameter('user'));"#g ./apps/frontend/modules/test/actions/actions.class.php

In config/ProjectConfiguration.class.php, enable the needed plugin:

public function setup()
{
  $this->enablePlugins('sfDoctrinePlugin');
}

For 1.0, there are some more steps:

Same as 1.2, except some changes in the initial task names:

php ./lib/vendor/symfony/data/bin/symfony new test
php symfony app frontend
php symfony module frontend test

In 1.0 versions, the actions file name as been renamed:

Also, the request object is not given as an argument to the controller, and it needs to be fetch manually from sfContext singleton by the developper:

class testActions extends sfActions
{
  public function executeIndex()
  {
    $request = sfContext::getInstance()->getRequest();
    $a = unserialize($request->getParameter('user'));
  }
}

Finally, you need to remove some deprecated PHP options so that your lib/vendor/symfony/data/config/php.yml will look like this:

set:
  log_errors:                  on
  arg_separator.output:        |
    &amp;

check:

warn:
  session.auto_start:          off
cfreal commented 6 months ago

Hello darkpills,

Thanks to your detailed infos I was able to make it work! Everything works fine.

Thank you for you contribution! Charles

darkpills commented 6 months ago

Hello @cfreal thank you for your time for testing! Another PR coming quickly I hope.