Adminer (formerly phpMinAdmin) is a full-featured database management tool written in PHP. Conversely to phpMyAdmin, it consist of a single file ready to deploy to the target server. Adminer is available for MySQL, MariaDB, PostgreSQL, SQLite, MS SQL, Oracle, Firebird, SimpleDB, Elasticsearch and MongoDB.

Download the software here.

Setup

After playing with the latest version of the software, there were certain issues that I ran into that should be fixed before proper functionality of the challenge. The two main challenges being password lockouts (as playing the waiting game isn't fun), and how the application uses response codes to determine how it should respond.

For the first issue, I was able to remove the password lockout, by modifying the source code to not include the if statement, that determines if a user has tried to login too many times.

  1. Take the PHP file and run it through a PHP beautifier.
  2. Locate the "check_invalid_login" function.
  3. Comment out the if statement that checks if too many failed attempts have been made:
function check_invalid_login()
{
    global $b;
    $ae = unserialize(@file_get_contents(get_temp_dir() . "/adminer.invalid"));
    $Zd = $ae[$b->bruteForceKey() ];
    $hf = ($Zd[1] > 29 ? $Zd[0] - time() : 0);
    if ($hf > 0) auth_error(lang(83, ceil($hf / 60)));
}

Get's turned into

function check_invalid_login()
{
    global $b;
    $ae = unserialize(@file_get_contents(get_temp_dir() . "/adminer.invalid"));
    $Zd = $ae[$b->bruteForceKey() ];
    $hf = ($Zd[1] > 29 ? $Zd[0] - time() : 0);
    /*if ($hf > 0) auth_error(lang(83, ceil($hf / 60)));*/
}

This will essentially completely disable the check that verifies if too many invalid login attempts have been made, rendering the function useless.

My next issue was response codes. I wanted the players to be able to both leak the source code, and interact with a backend service through the same adminer application.

Now, the reason this is an issue, is due to the fact that the adminer application focuses on whether the response code starts with 2XX or not. If it returns anything other than a 2XX code, it displays the source code of the application that answered with said code, else it will just return basic response messages such as "invalid login".

To fix this, I configured the back end to ONLY respond with a 2XX code, if a password has been specified. This allows for the players to hit the backend application and leak source code by not specifying a password, and once obtaining the source code, they can login by including the password parameter.

<?php

if($_SERVER['PHP_AUTH_PW'] == ""){
    http_response_code(1337);
}else{
    http_response_code(200);
}

Next, we needed to create an internal application that can be abused via the SSRF, I decided to go with a basic twig SSTI, to avoid guess-work within the challenge.

<?php
require_once('vendor/autoload.php');
require_once('config.php');

if (isset($_POST['name'])){
$loader = new \Twig\Loader\ArrayLoader([
    'index' => $_POST['name'],
]);
$twig = new \Twig\Environment($loader);
echo $twig->render('index', ['name' => 'Fabien']);
} else {
    echo highlight_file(__FILE__, true);
}

This was hosted on all interfaces, port 81. I ensured it was only accessible if the remote host requesting the page was localhost.
/etc/apache2/sites-enabled/000-default.conf

<VirtualHost *:81>
        ServerAdmin webmaster@localhost
        DocumentRoot /var/www/internal

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

We also need to modify what ports the apache2 server should listen on:
/etc/apache2/ports.conf

Listen 80
Listen 81

Finally, restart the service, and the internal application is up and running. The challenge is all set.

The bypass for CVE-2018-7667 and POST parameter smuggling

CVE-2018-7667 states that an SSRF exists in Adminer through 4.3.1, and has supposedly been patched since. The patch consists of simply restricting the access to ports below 1024. This check can be found within the PHP file here:

if (isset($_GET["username"]) && is_string(get_password()))
{
    list($Ed, $ig) = explode(":", SERVER, 2);
    if (is_numeric($ig) && ($ig < 1024 || $ig > 65535)) auth_error(lang(93));
    check_invalid_login();
    $h = connect();
    $m = new Min_Driver($h);
}

As you can see, it explodes upon :, meaning it will split a string using : as the delimiter, and then place each field into an array, selecting the second value. Said value is then checked to see if it is greater than 65535 or less than 1024, in which case it would return an authentication error. The trick is, the if statement uses "is_numeric", so if we pass a value to this check that includes anything other than just numbers, it will not pass the check, and the error will not be returned. So as long as we can find input for the server field, which puts a non-numeric value in the port field, but still follows valid URL syntax, we can bypass the check.

Let's walk through this with an example bypass:

  1. Input our payload localhost:80# or localhost:80/ into the server field.
  2. The get username and password check pass as these are set by default, even if empty.
  3. The application then explodes the server value upon ":", with a limit of input values of 2, $Ed would be the first part of the host, and $ig would be the port value.
  4. The check that makes the application vulnerable then comes into play, it takes $ig (80#), and sees if the value is numeracle, which it is not, due to the hash, so it passes the error and tries to connect to the server.
  5. Since the # is valid URL syntax for a comment, the file_get_contents that is later called doesn't error out, it just ignores anything after the hash.

So now we can hit any service and any port running internally, I then wanted to see to what extent I could take the application, so I started collaborator, and made it interact with my collaborator instance.

So the default data transfer of the clickhouse driver seems to be just sending POST binary data, such as below:

We essentially control anything at the bottom of the request via the "SQL Command" feature within Adminer, meaning we can smuggle POST parameters:

Would then become:

I tried playing with the URL parsing in the server header and CRLF injection to create headers of my own, but had no luck. Note, you can also include values in the "username" and "password" fields to authenticate against the internal services using basic authentication.

Solution

We start off with the knowledge of both applications (ports 80 and 81), so we can easily tell that the aim of the challenge is to abuse adminer to gain SSRF to port 81, where we will be able to get RCE.

The attacker would then need to find the previously stated SSRF security bypass to leak the sourcecode from port 81, as demonstrated below:

We can paste the HTML into a file and open it in our browser to have a clear view of what was returned:

We appear to have a basic twig app running on port 81, which is vulnerable to Twig SSTI, so we need a way to POST a variable called "name" to the backend server, with our twig payload, for blind remote command execution.

We set any username and password, to make the application accept our authentication (these can be any values, they are not relevant to the challenge). Next, in the SQL Command page of the application, we can use the POST data smuggling to send the name variable to the application.

We can see in burp that the application sleeps for 5 seconds, meaning we have blind command execution:

Finally, to exfiltrate the flag, we can use a variety of techniques, if outbound network activity is allowed, then we can exfiltrate command output via SMTP or even HTTP requests, as demonstrated below:

We receive the flag in our collaborator instance:

Another method for data exfiltration would be bash blind command exfiltration. The basis for this type of exfiltration would be to create a script that can automatically determine if a command returns output or not based on the time the application sleeps.