Hackme results! | News | PHPro
What's new in our world

Hackme results!

16 August 2016

Our contest has ended, thanks to all for participating. There were a lot of you :-)

Gabriel and Bruno have been picked from the raffle and have been contacted. They will receive their SSD Disk shortly!

How you could have hacked the website:

  1. The source code of the pages contains the book Moby Dick. At the end you can see the line: 'That was fun ... If only we could add some layout to the book'.
  2. The line above points at the included CSS stylesheet styles.css. If you reformat the minified CSS, you are able to find a link to the github repository: 'https://github.com/phpro/security-workshop'.
  3. In the github repository you can see that the image is loaded from the database, but there are no database credentials available. So the next step is to find out where the database credentials are stored.
  4. In the commit history, you can find a commit titled 'Removed DB connection from index file.'. The changeset contains the database credentials of the server but you won't be able to connect on the port 9999.
  5. Since you can't connect on port 9999, you'll have to find out which port is opened. This can be done with e.g. the command 'nmap hackme.phpro.be'. This will return port 9998.
  6. At this point you will be able to connect to the database. You won't be able to read the images table but a view imagesColumns is available.
  7. Based on the imagesColumns view you will be able to create an INSERT statement and therefore change the image on the hackme website.


Securing the hackme application.

Since this contest started out as an internal project, we've put a lot of focus on the flow on how to hack the website.
It was just a little side project to inform our colleagues that some small mistakes can end up in a big catastrophe.
By focussing on the flow of the hackme contest, we forgot to secure the application for malicious contestants.
Off course, this was something that fired back to us on the first days of the competition.
Here is a little write-up of the problems we've encountered and how we fixed them.

One of the first hackers of the website noted that the website was vulnerable for XSS attacks.
It was pretty naive of us to think that the hacker won't try to inject HTML or JavaScript code to the application.
Adding input validation was not possible since the user has direct insert permissions on the database.
So the only thing we could do is add output filtering by using the htmlspecialchars() method in PHP.

A little piece of JavaScript was added to make sure that if an image could not be loaded, a demotivational anti-xss image was shown:

;(function($) {
  $(function() {
    var image = $('#hackme-image');
    var hackedUrl = image.data('src');
    var failedUrl = '/xss-breakfast.jpg';
      .on('error', function() {
        image.attr('src', failedUrl);
      .attr('src', hackedUrl)

I eat your XSS for breakfast

More information on output escaping.

A few hours later, it looked like this type of protection was not enough.
One of the contestants inserted the image URL:  http://superlogout.com/ which resulted in getting logged out in frequently used services like gmail and Facebook.
Another one of the contestants added JavaScript to an SVG image: 

<svg width="580" height="400" xmlns="http://www.w3.org/2000/svg">
  <rect fill="#fff" id="canvas_background" height="402" width="582" y="-1" x="-1"/>
  <g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
   <rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
  <title>Layer 1</title>
 <text xml:space="preserve" text-anchor="start" 
font-family="Helvetica, Arial, sans-serif" font-size="24" id="svg_1" 
y="201" x="232.5" stroke-width="0" stroke="#000" 
 <script type="text/javascript">
      alert('This site is vulnerable to XSS attacks!');

The JavaScript gets executed when a contestant visits the website. 
This means that we'll have to protect against this type of attacks and improve our simple htmlspecialchars output filtering.
We've done this by validating if the user added a valid URL. The extension of the file was validated by a whitelist of allowed extensions:

More information about validating uploaded files.

The security we've added ended up looking like this: 

function xssProtect($url) {
    $invalidUrl = '/xss-breakfast.jpg';
    $allowedExt = ['jpg', 'jpeg', 'gif', 'png', 'bmp', 'tiff'];
    $url = htmlspecialchars($url);
        return $invalidUrl;
    $parsed = parse_url($url, PHP_URL_PATH);
    if (!$parsed) {
        return $invalidUrl;
    $file = basename($parsed);
    $fileParts = pathinfo($file);
    $extension = isset($fileParts['extension']) ? strtolower($fileParts['extension']) : '';
    if (!in_array($extension, $allowedExt)) {
        return $invalidUrl;
    return $url;

Of course this type of protection is not enough and has some flaws:

  • It is possible to use for example SVG content with the file extension JPG. Normally your browser protects you against this type of attacks and the JavaScript makes sure the demotivational anti XSS image is displayed. If you are using a very old browser, it is possible that this type of attacks can be executed. So this is something we did not focus on. The only way to get around this issue is by downloading the file and inspecting the content of the file with a library like imagemagick. Since this is a lot of work and requires a lot of local storage we decided not to focus on this issue.
  • It is not possible to add endpoints that don't contain an image prefix. This means that short URLs are not possible.

By adding this type of prevention, we managed to demotivate new contestants in finding additional XSS holes.
You might think that our problems where solved at this time, but guess again ... The next wave of issues was there.
Since we are loading the last image based on ID sorting DESC, there was someone that inserted a record int the last possible ID of the mysql INT type.
Mysql will always try to add 1 to the last inserted ID, which results in an integer overflow. 
This is an issue that should not happen in a normal application, so we temporarily patched this issue by making the field a mysql BIGINT type.
Of course this patch only rescued the application for exactly 2 minutes, so we had to think fast and come up with a bullet proof solution:

CREATE TRIGGER images_cleandata

  SET NEW.id = NULL;
  SET NEW.creation_date = CURRENT_TIMESTAMP;


After creating this trigger, we had to remove the id column and re-add it again to reset the auto increment values of the table.
When a user inserts a new record with a high id, it is reset to NULL by the trigger and the AUTO_INCREMENT options kicks in from that point.
It also made sure that the contestant could not tamper with the creation date.

At this point, the contestants started to get more aggressive by writing cronjobs or automated scripts to spam the mysql server with insert commands. 
This hack was problematic since we wrote a cronjob that mails us every time there were new records in the images table. You can imagine how bloated my mailbox got at that point, so I decided to only send these mails once an hour.

The first time we've encountered these kind of bots, we thought we could stop these contestants from spamming the server by replacing the image url with a demotivational crons image.

CREATE TRIGGER images_cleandata

  -- Try to avoid random query strings:
  set @baseUrl := (SELECT SUBSTRING_INDEX(NEW.imageURL, '?', 1));

  -- Search for duplicate records:
  SET @croncount := (
    FROM images
    WHERE (
      imageURL = NEW.imageURL
      OR imageURL LIKE CONCAT(@baseUrl, '?%')
      OR imageURL = @baseUrl
    AND `name` = NEW.name
    AND email = NEW.email
    AND location = NEW.location

  -- Overwrite anti cron / bot image
  IF @croncount >= 10 THEN
    SET NEW.imageURL = 'http://hackme.phpro.be/cron-nam.png';

  -- Set defaults:
  SET NEW.id = NULL;
  SET NEW.creation_date = CURRENT_TIMESTAMP;


I have fought harder crons in Vietnam

As you can see, the cron now looks for image URLs that occurred 10 times in the database. If the image was inserted more then 10 times, the demotivational cron image gets displayed.
At some later point we also needed to add the baseUrl check since the bots were getting smarter and where adding some random hash in the query part of the URL. This fix made some of the bots stop, but some other bots just did not notice so we needed to find out from which IP they were spamming the server. We did this by adding a new host field to the images table and also filled this field by altering the trigger:

CREATE TRIGGER images_cleandata
  -- Look for host:
  set @host := (SELECT SUBSTRING_INDEX(USER(), '@', -1));
  -- ... snap ...
  -- Set defaults:
  SET NEW.host = @host;

At first we've used the host field from the `information_schema.processes` table. Normally this should work, but since the code is run inside a trigger, the current host is always localhost:randomport. After some looking around we noticed that we could use the built-in `USER()` function which returns the name of the mysql user and the name of the actual host. For example: hackMe@myhost.

Now that we've got the host of the contestant and get notified about spamming bots, it is easy to ban the ip using iptables.
We did not automate this process to make sure that no contestant gets banned for a crime they did not commit.

iptables -A INPUT -s yourhost -p tcp --destination-port 9998 -j DROP

After we applied this fix, the bots were getting smarter again and only inserted a new image when the image on the website changed since the last time they checked. Since they were constantly looping between 2 or 3 images, it was easy to determine if it was a bot that was inserting the images or a real user. If we found out that is was a bot, we ip banned the bot and removed the inserted record so that the last real image was displayed again.

As you can imagine, it was a long road to fully protecting this very very simple application.
The lessons we've learned from this project is: Don't take security for granted!
Real hackers will always find a way to destroy your application. 
It's all about monitoring, the counter measures you take and the amount of time you need to implement these protections!