Flatt Security Blog

株式会社Flatt SecurityのBlogです。

Technical descriptions of 0days found - EC-CUBE, SoyCMS, BaserCMS

f:id:flattsecurity:20201030185627p:plain

Hi, I’m stypr (@stereotype32) from Flatt Security Inc.

As I mentioned earlier in the previous article「Flatt Securityは“自分のやりたいことが実現できる”場所/セキュリティエンジニア stypr - Flatt Security Blog)」, I’m currently focusing on technology research and 0day hunting in Flatt Security.

This article will show you a detailed explanation about 0days I found since I joined this May. I was able to find a lot of vulnerabilities so far, and I am going to show you some of the vulnerabilities which I found pretty interesting while finding and exploiting them.

I’ve been spending a day to a week on each product I’m working on, as I’ve enjoyed doing a lot of other interesting tasks apart from the 0day hunting. Anyways, I was able to find a lot of vulnerabilities, but many of them have not been fixed yet. Once these projects fix vulnerabilities, I will publish further articles soon. The first target I’ve worked on were OSS products that are frequently used in Japan. This is because there are so many open source products that ships with different environments.

This article will introduce some of the reported vulnerabilities that I’ve proved to be exploitable by writing a PoC(Proof-of-Concept) code. Regarding PoC code, I decided not to disclose it as I found out that some of the exploits are actually exploitable in the wild. Instead of publishing a PoC code, I’ve made videos of running the PoC code.

I want to thank the developers, JPCERT/CC team, and most importantly, my co-workers who greatly helped me a lot while reporting these vulnerabilities.

BaserCMS

What is BaserCMS

github.com

BaserCMS is a Japanese Open-sourced CMS platform to create websites easily. It was developed on top of CakePHP, and it is actively under development and used in many services.

(CVE-2020-15159) Cross-site Scripting (XSS) and Remote Code Execution (RCE)

Pre-conditions

The following pre-condition(s) is required to exploit this vulnerability successfully.

  • User with the administrator privilege is required to access the attacker’s malicious website/link.
Writeup

Let me start with the Cross-Site Scripting(XSS). In usual cases, there are many ways to find a typical XSS vulnerability. But in most root causes of XSS, it is usually because the user input has not been sanitized properly. So to find XSS vulnerabilities, it would be better off to start looking for the existence of user input sanitization functions.

For example, app/webroot/theme/admin-third/ThemeFiles/admin/index.php:48 prints the current path as the following code.

<div class="em-box bca-current-box"><?php echo __d('baser', '現在の位置') ?>:<?php echo $currentPath ?>

When we trace the currentPath, we can see that currentPath is taken from
lib/Baser/Controller/ThemeFilesController.php:171 .However the current path is not sanitized at all. Thus we can trigger the XSS vulnerability.

public function admin_index() {
...
        $args = $this->_parseArgs(func_get_args());
        extract($args);
...
        $currentPath = str_replace(ROOT, '', $fullpath);
        $this->subMenuElements = ['theme_files'];
        $this->set('themeFiles', $themeFiles);
        $this->set('currentPath', $currentPath);
        $this->set('fullpath', $fullpath);
...
        $this->help = 'theme_files_index';
...
}
...

Now I’ll explain the Remote Code Execution(RCE) vulnerability. In most typical PHP products, it is possible to load arbitrary code when the user can upload arbitrary PHP files, and the user is aware of the uploaded file’s path.

There is an upload function in lib/Baser/Plugin/Uploader/Controller/UploaderFilesController.php:291

 public function admin_ajax_upload() {
...
        $this->layout = 'ajax';
        Configure::write('debug',0);
...
        $user = $this->BcAuth->user();
        if(!empty($user['id'])) {
            $this->request->data['UploaderFile']['user_id'] = $user['id'];
        }
        $this->request->data['UploaderFile']['file']['name'] = str_replace(['/', '&', '?', '=', '#', ':'], '', h($this->request->data['UploaderFile']['file']['name']));
        $this->request->data['UploaderFile']['name'] = $this->request->data['UploaderFile']['file'];
        $this->request->data['UploaderFile']['alt'] = $this->request->data['UploaderFile']['name']['name'];
        $this->UploaderFile->create($this->request->data);
...
        if($this->UploaderFile->save()) {
            echo true;
        }
...
...
    }

The upload feature checks for the filename but never checks about the file extension.

To look for further detailed information, We can see the following code (lib/Baser/Plugin/Uploader/Model/UploaderFile.php) by checking $this->UploaderFile

class UploaderFile extends AppModel {
...
    public function __construct($id = false, $table = null, $ds = null) {
...
        if(!BcUtil::isAdminUser()) {
            $this->validate['name'] = [
                'fileExt' => [
                    'rule' => ['fileExt', Configure::read('Uploader.allowedExt')],
                    'message' => __d('baser', '許可されていないファイル形式です。')
                ]
            ];
        }
        parent::__construct($id, $table, $ds);
...
    }
}

It does check for the file extension, but that only applies when the user does not have the administrator privilege. This means that uploading PHP files is possible when the administrator runs arbitrary scripts within the domain.

PoC and Reference

It is possible to upload files by using Blob Object(https://developer.mozilla.org/ja/docs/Web/API/Blob) in Javascript.

With this in mind, we can upload files from the XSS and add arbitrary PHP codes to the server.

...
    filename = Math.floor(Math.random() * Math.floor(13371337)) + 'exploit.php';
...
    var blob = new Blob(["stypr@flatt<pre><?php $_GET[cmd]($_GET[arg]); ?></pre>"]);
    var fd = new FormData();
    fd.append('data[_Token][key]', token);
    fd.append('data[UploaderFile][file]', blob, filename);
    // Upload XHR Request
...

You can also check the following PoC video to see that this vulnerability is exploitable.

https://i.imgur.com/J3ZnpZg.gif

Response

The developer has responded immediately and CVE is assigned to this vulnerability.

https://basercms.net/security/20200827

https://nvd.nist.gov/vuln/detail/CVE-2020-15159:

github.com

EC-CUBE

What is EC-CUBE

https://www.ec-cube.net/

github.com

EC-CUBE is an Open Source product that is most widely used by companies in Japan.

It’s developed on top of Symfony, and the product is under very active development. I was looking for a new target after BaserCMS, and Mr. udon has recommended me as a next target. I decided to check on it for a while.

f:id:flattsecurity:20201021204522p:plain

Unauthenticated/Authenticated Remote Code Execution (RCE)

Pre-conditions

The following pre-condition(s) is required to exploit this vulnerability successfully.

  1. Unauthenticated RCE: The environmet should haveAPP_DEBUG=1 * This can be reproduced by a default docker installation

  2. Authenticated RCE: User needs to have an administrator privilege and needs to access a malicious webpage / website within the same domain scope.

Writeup

Let’s assume that we can exploit this vulnerability without any authentication. We need APP_DEBUG=1 to exploit this vulnerability, but we can easily reproduce this by installing through the Docker setup as suggested by the official installation guideline.

$ # https://doc4.ec-cube.net/quickstart_install#3dockerを使用してインストールする
$ git clone https://github.com/ec-cube/ec-cube
...

$ cd ec-cube; git checkout c1dbe4267e1a3f353522835a22793aa278f42ef3 # 脆弱性報告時点
Note: switching to 'c1dbe4267e1a3f353522835a22793aa278f42ef3'.
...

$ docker build -t eccube4-php-apache .
Sending build context to Docker daemon  23.16MB
Step 1/21 : FROM php:7.3-apache-stretch
...

$ docker run --name ec-cube -p "8080:80" -p "4430:443" eccube4-php-apache
...

^C
$ docker start ec-cube
ec-cube
$ docker exec -it ec-cube bash
root@e70e14d8e327:/var/www/html# cat .env | grep DEBUG
APP_DEBUG=1

Once you checked that the server is set up correct as the snippet above, let’s access the website by accessing http://[Instance_IP]:8080/

f:id:flattsecurity:20201021204553p:plain

As you see the screenshot above, there is sf logo on the right bottom side of the page. This logo is shown when the Symfony is under the debug mode. There are some cases that this logo doesn’t show up, so try accessing /_profiler and you will see the page as shown below

f:id:flattsecurity:20201021204605p:plain

This feature is called Symfony Profiler, and there is not much information about this feature on the internet. The intention of this feature crystal clear; it helps you debug when there is an error or a bug. Of course, this feature can only be used when the debug mode is enabled.

The Symfony framework itself is very secure, but enabling debug mode will make this framework will make it extremely vulnerable. For example, Profiler has a feature called Profile Search, as the following screenshot.

f:id:flattsecurity:20201021204624p:plain

As you see in the screenshot above, you can access all sent requests to the server. By clicking hashes in the token, you will see that all POST parameters can be read, as seen in the following screenshot. With this feature, we can hijack the administrator and user’s account credentials.

f:id:flattsecurity:20201021204637p:plain

Let’s now assume that we can authenticate as an administrator by using this vulnerability. I’m going to explain about the RCE part. If you just look around this administrator page, there is a feature called File management.

f:id:flattsecurity:20201021204700p:plain

The approach is the same as BaserCMS. It is possible to execute an arbitrary PHP code when it is possible to upload a PHP file and when it is possible to predict the file path of the uploaded file.

There are some security restrictions (such as $jailNowDir) implemented in this file upload just like src/Eccube/Controller/Admin/Content/FileController.php:52 but it never checks for extensions and moves the uploaded files to the upload directory.

    public function index(Request $request)
    {
        $form = $this->formFactory->createBuilder(FormType::class)
            ->add('file', FileType::class, [
                'multiple' => true,
                'attr' => [
                    'multiple' => 'multiple'
                ],
            ])
            ->add('create_file', TextType::class)
            ->getForm();

        // user_data_dir
        $userDataDir = $this->getUserDataDir();
        $topDir = $this->normalizePath($userDataDir);
...
        $htmlDir = $this->normalizePath($this->getUserDataDir().'/../');
...
        $nowDir = $this->checkDir($this->getUserDataDir($request->get('tree_select_file')), $this->getUserDataDir())
            ? $this->normalizePath($this->getUserDataDir($request->get('tree_select_file')))
            : $topDir;
...
        $nowDirList = json_encode(explode('/', trim(str_replace($htmlDir, '', $nowDir), '/')));
        $jailNowDir = $this->getJailDir($nowDir);
        $isTopDir = ($topDir === $jailNowDir);
        $parentDir = substr($nowDir, 0, strrpos($nowDir, '/'));
...
        if ('POST' === $request->getMethod()) {
            switch ($request->get('mode')) {
...
                case 'upload':
                    $this->upload($request);
                    break;
...
            }
        }
...
    }
...
    public function upload(Request $request)
    {
        $form = $this->formFactory->createBuilder(FormType::class)
            ->add('file', FileType::class, [
                'multiple' => true,
                'constraints' => [
                    new Assert\NotBlank([
                        'message' => 'admin.common.file_select_empty',
                    ]),
                ],
            ])
            ->add('create_file', TextType::class)
            ->getForm();
        $form->handleRequest($request);
...
        if (!$form->isValid()) {
            foreach ($form->getErrors(true) as $error) {
                $this->errors[] = ['message' => $error->getMessage()];
            }

            return;
        }
...
        $data = $form->getData();
        $topDir = $this->getUserDataDir();
        $nowDir = $this->getUserDataDir($request->get('now_dir'));
...
        foreach ($data['file'] as $file) {
            $filename = $this->convertStrToServer($file->getClientOriginalName());
            try {
                $file->move($nowDir, $filename);
                $successCount ++;
...
    private function getUserDataDir($nowDir = null)
    {
        return rtrim($this->getParameter('kernel.project_dir').'/html/user_data'.$nowDir, '/');
    }

Interestingly, the developers generated a .htaccess file to harden the security, but these are only limited to the composer, docker-related files, and important files.

.htaccess

<FilesMatch "^composer|^COPYING|^\.env|^\.maintenance|^Procfile|^app\.json|^gulpfile\.js|^package\.json|^package-lock\.json|web\.config|^Dockerfile|\.(ini|lock|dist|git|sh|bak|swp|env|twig|yml|yaml|dockerignore)$">
    order allow,deny
    deny from all
</FilesMatch>

With this vulnerability, we can upload a PHP file and execute arbitrary code in http://[host]/user_data/[filename].php

PoC

PoC will work once you finish developing the code under this routine

  1. Leak ID, PW by snatching traffics of admin folder’s directory using the Symfony Profiler.* In case you failed to trigger this, you can try accessing admin by admin/password (default setup)

  2. Upload PHP files by accessing admin based on snatched credentials

  3. Execute PHP code by loading the uploaded PHP file from html/user_data/

You can also check the following PoC video to see that this vulnerability is exploitable.

Response and alerting customers

I’ve discussed a long time with the developer regarding this vulnerability. The developer has asked many other companies, and they concluded not to regard this as a vulnerability. The reason is as follows:

  1. PHP file upload in administrator privilege is a specification for EC-CUBE.

  2. The debug mode is only enabled from the official guideline but is never triggered in the production mode.

  3. Developers have been alerting customers, sharing security-related plugins and security checklists for a long time.

    *https://www.ec-cube.net/products/detail.php?product_id=2040

    *https://doc4.ec-cube.net/environmental_setting

When writing this article, I found out that many websites are vulnerable to this attack, and EC-CUBE team is aware of the current situation. As a result, the developers decided to be more alert about this issue and allowed me to share this issue with the condition of not sharing the exploit PoC. To fix this issue, the customer needs to ensure that the debug mode is always off outside the localhost.

github.com

Please refer to the following link for more detailed conversation thread.

blog.harold.kim

SoyCMS

What is SoyCMS

https://saitodev.co/soycms/

github.com

It is a highly-customizable CMS that can be used to develop a blog or an online mall. It is free to use as source codes are published as open-source. I found this product while searching on the internet. I decided to try on this product as the product's name was cute and the developer was actively working on the product.

(CVE-2020-15183) Cross-site Scripting (XSS) leading to Remote Code Execution (RCE)

Pre-conditions

The following pre-condition(s) is required to exploit this vulnerability successfully.

User with the administrator privilege is required to access the attacker’s malicious website/link.

Writeup

Let’s start with the XSS vulnerability.

When we the code in cms/app/webapp/inquiry/admin.php, we see the following code.

class SOYInquiryApplication{
...
    function init(){
        $level = CMSApplication::getAppAuthLevel();
...
        CMSApplication::main(array($this,"main"));
...
    }
...
    function main(){
...
        $arguments = CMSApplication::getArguments();
...
        foreach($arguments as $key => $value){
            if(is_numeric($value)){
                $flag = true;
            }

            if($flag){
                $args[] = $value;
            }else{
                $classPath[] = $value;
            }
        }
        $path = implode(".",$classPath);
        $classPath = $path;
...
        if(preg_match('/^Help/',$classPath)){
            CMSApplication::setActiveTab(4);
        }

        if(!SOY2HTMLFactory::pageExists($classPath)){
            return $classPath;
        }
...
}

$app = new SOYInquiryApplication();
$app->init();

It returns $classPath when the page does not show up, so I felt strange about this and found XSS by trying many tests here. To find the root cause, I’ve traced the code from the initialization part.

The application first starts with CMSApplication::run() from (https://github.com/inunosinsi/soycms/blob/41931745c680cb1cfc3d5e18d19c7e765a712bc1/cms/app/index.php#L9)

<?php
define("CMS_APPLICATION_ROOT_DIR", dirname(__FILE__) . "/");
define("CMS_COMMON", dirname(dirname(__FILE__)) . "/common/");

include_once(dirname(__FILE__)."/webapp/base/config.php");

try{
    //アプリケーションの実行
    CMSApplication::run();

    //表示
    CMSApplication::display();

}catch(Exception $e){
    $exception = $e;
    include_once(CMS_COMMON . "error/admin.php");       
}

After that, there is the following code in CMSApplication class. (https://github.com/inunosinsi/soycms/blob/41931745c680cb1cfc3d5e18d19c7e765a712bc1/cms/app/webapp/base/CMSApplication.class.php)

class CMSApplication {
...
        public static function run(){
                $self = CMSApplication::getInstance();
                $self->root = SOY2PageController::createRelativeLink("./");
....
                //pathinfoからアプリケーションIDを取得
                $pathinfo = (isset($_SERVER["PATH_INFO"])) ? $_SERVER["PATH_INFO"] : "";
...
                $paths = array_values(array_diff(explode("/",$pathinfo),array("")));
                if(count($paths)<1){
                        SOY2PageController::redirect("../admin/");
                        exit;
                }
                $self->applicationId = $paths[0];
                $self->arguments = array_slice($paths,1);
...
                $cacheDir = dirname(dirname(dirname(__FILE__)))."/cache/".$self->applicationId."/";
...
                //アプリケーションの読み込み
                include_once($base . $self->applicationId . "/admin.php");
...
                $self->application = call_user_func($self->appMain);
         }
        public static function display(){
                $self = CMSApplication::getInstance();
                include_once(dirname(__FILE__) . "/" . $self->mode . ".php");
        }
        public static function main($func){
                $obj = CMSApplication::getInstance();
                $obj->appMain = $func;
        }
...

        public static function display(){
                $self = CMSApplication::getInstance();
                include_once(dirname(__FILE__) . "/" . $self->mode . ".php");
        }

...
}

The following is the steps of execution until $classPath is modified.

Application starts off with app/index.php and CMSApplication::run(); is called. When we summarize the important part from the steps executed fromrun() , the step is as follows.

  1. $pathinfo = $_SERVER["PATH_INFO"] is the path of the executed file.For example,http://hoge.com/index.php/inquiry/blah will make $pathinfoto /inquiry/blah

  2. Through $pathinfo , variables like $self->arguments , $self->applicationId are assigned

  3. Based on $self->applicationId, $base . "inquiry/admin.php" is loaded as a script

    1. When we check the bottom-most part of the code in SOYInquiryApplication, $app = new SOYInquiryApplication(); $app->init();is executed.

    2. While looking for the way things are executed in $app->init() , it calls for CMSApplication::main(array($this,"main"));

      • When CMSApplication::main(array(SoyInquiryApplication,”main”)); is executed, the object’s appMain changes as $obj->appMain = Array(SoyInquiryApplication, ”main”);
    3. Finally, SoyInquiryApplication->main() is finally called from $self->application = call_user_func($self->appMain);

      • $classPath is generated based on $arguments = CMSApplication::getArguments();
      • However, when the page does not exist, return $classPath will be return directly.

Finally, $self->application = $classPath is assigned to the $self->application.

The problem here is as follows:

  1. There is nothing about sanitization or checks in $pathinfo

  2. When $classPath is wrongly generated by $pathinfo, the value will be returned directly.

  3. As a result, $self->application = $classPath will have a direct value from $pathinfo and gets assigned.

After the process above,CMSApplication::display(); is executed and include_once(dirname(__FILE__) . "/" . $self->mode . ".php");is called as a result. In here, $self->mode is the default template. By looking a bit on this source code, it looks like the following.

...
<div id="tabs" class="content-wrapper">
    <?php CMSApplication::printTabs(); ?>
</div>

<div id="content" class="content-wrapper last"><?php CMSApplication::printApplication(); ?></div>
...

By checking the CMSApplication::printApplication(), we see the following code.

        public static function printApplication(){
                $self = CMSApplication::getInstance();
                echo $self->application;
        }

Yes, The thing is that $self->application = $classPathwill have the user input, the user input can insert HTML tags, and this leads to the execution of Javascript in victim’s page.

f:id:flattsecurity:20201021204729p:plain

f:id:flattsecurity:20201021204750p:plain

Let’s continue to the RCE part. I mentioned that this vulnerability is “leading to RCE”, and this is because the vulnerability is exploited based on the known issue in GitHub

github.com

f:id:flattsecurity:20201021204802p:plain

There is an issue where the administrator can change the index.php source code arbitrarily, and this vulnerability has not been fixed during exploitation. By using this vulnerability, it was possible to achieve a working XSS to RCE.

Response

I’ve sent a PR request as the developer was working on the project alone.

For this case, I’ve fixed the RCE by adding CSRF tokens and checking referrers.

github.com

(CVE-2020-15188) Remote Code Execution (RCE)

Pre-conditions

The following pre-condition(s) is required to exploit this vulnerability successfully.

  • Attacker has the administrator privilege.

  • User with the administrator privilege is required to access the attacker’s malicious website/link. CVE-2020-15183 can be used to trigger this attack.

Writeup

It is another RCE gadget that can be chained with the XSS bug I mentioned earlier.

It’s not something cool, but it was interesting that the external plugin elFinder may have security pitfalls.

For example, there is the following option in SoyCMS. soycms/js/elfinder/php/connector.php:143-159

$opts = array(
    // 'debug' => true,
    'roots' => array(
        // Items volume
        array(
            'driver'        => 'LocalFileSystem',           // driver for accessing file system (REQUIRED)
            'path'          => $path,                        // path to files (REQUIRED)
            'URL'           => $url,                         // URL to files (REQUIRED)
            //'trashHash'     => 't1_Lw',                     // elFinder's hash of trash folder
            'winHashFix'    => DIRECTORY_SEPARATOR !== '/', // to make hash same to Linux one on windows too
            'uploadDeny'    => array('all'),                // All Mimetypes not allowed to upload
            'uploadAllow'   => array('image', 'text/plain', 'text/css', 'application/zip', 'application/epub+zip','application/pdf'),// Mimetype `image` and `text/plain` allowed to upload
            'uploadOrder'   => array('deny', 'allow'),      // allowed Mimetype `image` and `text/plain` only
            'accessControl' => 'access'                     // disable and hide dot starting files (OPTIONAL)
        ),
    )
);

As we see, it just checks for mimetype and never checks for the extension. There were many issues reported to elFinder, and the only suggested solution by far is to add the attributes option in $opts.

         'uploadOrder'   => array('deny', 'allow'),      // allowed Mimetype `image` and `text/plain` only
            'accessControl' => 'access',                     // disable and hide dot starting files (OPTIONAL)
            'attributes' => array(
                                //フロントコントローラー
                                array(
                                        'pattern' => '/\\.php(\\.old(\\.[0-9][0-9])?)?$/',
                                        'read' => false,
                                        'write' => false,
                                        'locked' => true,
                                        'hidden' => true,
                                ),
                        )
        ),
PoC

I’ve re-used the Blob Object I’ve used in BaserCMS. But for this time, I’ve also bypassed the mimetype by adding more information in Blob Object.

You can also check the following PoC video to see that this vulnerability is exploitable.

Response

I’ve sent a PR request as the developer was working on the project alone.

github.com

(CVE-2020-15189) Cross-site Request Forgery (CSRF) leading to RCE

Pre-conditions

The following pre-condition(s) is required to exploit this vulnerability successfully.

User with the administrator privilege is required to access the attacker’s malicious website/link.

Writeup

When we look for the code a bit, we see the following code as written in cms/app/webapp/inquiry/pages/Template/EditPage.class.php:7-26

 function doPost(){
        
        $target = $this->target;
        $dir = SOY2::RootDir() . "template/";
        if(!file_exists($dir . $target) || !is_writable($dir.$target)){
            CMSApplication::jump("Template");
            exit;
        }
        
        $path = $dir . $target;
        
        //bk
        $content = file_get_contents($path);
        file_put_contents($path . "_" . date("YmdHis"),$content);
        
        $content = $_POST["content"];
        file_put_contents($path,$content);
        
        CMSApplication::jump("Template");
        exit;
    }
    function __construct() {
        
        $target = @$_GET["target"];
        $this->target = $target;
        $dir = SOY2::RootDir() . "template/";
        if(!file_exists($dir . $target) || !is_writable($dir.$target)){
            CMSApplication::jump("Template");
            exit;
        }
        
        parent::__construct();
        
        $path = $dir . $target;
        
        $content = file_get_contents($path);
        
        $this->createAdd("target","HTMLLabel",array(
            "text" => $target
        ));
        
        $this->createAdd("content","HTMLTextArea",array(
            "name" => "content",
            "value" => $content
        ));
    }

The doPost method in the code above never checks for any CSRF token and inserts the file directly. With this, we can use CSRF attack to add malicious file from the attacker’s malicious page.

I’ve also found other vulnerabilities from this file, but I’ve silently patched it without getting a CVE.

PoC
Response

I’ve sent a PR request as the developer was working on the project alone.

github.com

(CVE-2020-15182) Unauthenticated Remote Code Execution (RCE)

Pre-conditions

The following pre-condition(s) is required to exploit this vulnerability successfully.

  • The server needs to have the default plugin (Soy Inquiry) enabled.

Most websites that run on Soy CMS use Soy Inquiry, and I’ve also confirmed that this vulnerability is actually exploitable in the wild. This vulnerability is the most critical one amongst 4 vulnerabilities.

Writeup

Let’s first assume that Soy Inquiry is working properly.

By checking cms/app/webapp/inquiry/page.php:126-133, we see the following code.

     if(isset($_POST["form_value"]) && isset($_POST["form_hash"])){
            $value = base64_decode($_POST["form_value"]);

            //不正な書き換えでない場合のみ
            if(md5($value) == $_POST["form_hash"]){
                $_POST["data"] = unserialize($value);
            }
        }

There are two main issues in this code.

1.md5($value) == $_POST["form_hash"]

The developer added md5($value) == $_POST["form_hash"] to prevent spams and illegal inputs, but as all of you know, md5 is not an encryption and it is just an one-way hash function. This means that we have control over $_POST["form_value"] and $_POST["form_hash"].

  1. $_POST["data"] = unserialize($value);

The most dangerous code after all. The PHP’s official manual has the following warning note .(https://www.php.net/manual/en/function.unserialize.php)

Do not pass untrusted user input to unserialize() regardless of the options value of allowed_classes. Unserialization can result in code being loaded and executed due to object instantiation and autoloading, and a malicious user may be able to exploit this.

As it says,unserialize()is a function that you should never use in your code. You can find more information about this vulnerability on the internet.

https://www.hack.vet/entry/20170328/1490712989

https://blog.tokumaru.org/2015/07/phpunserialize.html

https://blog.ohgaki.net/how-to-use-php-serialize-unserialize-with-security

It was found out that there were a lot of classes loaded before unserialize(), and it was possible to execute an arbitrary code by utilizing dangerous classes.

PoC and Tips

I’ve made the following code to create a PoC and successfully executed a PHP code. I think it will be possible to generate an exploit with a shorter payload. Give it a try if you’re interested in creating a shorter payload.

<?php

class ClassHoge2 {
...
}

class ClassHoge1 {
...
}

$exploit = serialize(Array(
    "column_1" => ...ClassHoge1,
    "column_2" => "b@b.com",
    "column_3" => "c",
    "column_4" => "e",
    "column_5" => "e",
    "column_6" => "f",
    "hash" => "1337",
    "captcha" => "1337",
));

$form_hash = md5($exploit);
$form_value = base64_encode($exploit);

echo "form_hash = '$form_hash'\n";
echo "form_value = '$form_value'\n";

?>

To exploit unserialize() faster and efficiently, it is good to add var_dump(get_declard_classes()); before the unserialize() function is executed, so that you get which classes can be used for exploitation. This will eventually save time and find gadgets more efficiently.

You can also check the following PoC video to see that this vulnerability is exploitable.

Response

I’ve sent a PR request as the developer was working on the project alone.

github.com

Conclusion

I’m really sorry for making a very long technical post.

I've introduced 6 vulnerabilities in 3 products, and I still have 25+ vulnerabilities that are not fixed at the time of writing the article. I will post further details about other vulnerabilities once fixes are finally made.

Thank you so much for taking your time to read the article.