For this challenge, reyammer and I joined forces and went more or less into pair programming mode. Half of the credit definitely belongs to him :)
Since we were given a web application, our first instinct was to look at the cookies that were set once we have logged in. Luckily for us, this was exactly where we found an exploitable vulnerability (we also found an XSS vulnerability in the value field of the widget, which was a nice decoy and after dumping a few widgets for fun's sake, it seems that quite a few teams were stuck on trying to XSS it).
Upon login, the webapp sets three cookie values: PHPSESSID, widget_tracker and widget_validate. PHPSESSID looks very much normal and we decided do not look into it further for now. Instead, we looked at the interesting two values widget_tracker and widget_validate:
widget_tracker very much looks like a base64-encoded string and widget_validate looks very much like a SHA512 checksum. We checked if the checksum is of the base64 encoded string, but that was not the case, instead, it was simply the checksum of the decoded string. The validation was not an obstacle anymore. The decoded string simply looks like the following:
Those numbers matched perfectly the ids of the widgets we just created for test purposes. Clearly, our first step was modifying those ids and see if we can request arbitrary widgets for which we do not have the proper permissions. This did indeed work. We figured that this corresponds to an array with the first i being the index and the second i being the content, probably i stands for integer. After some brainstorming, we looked up how PHP serializes and unserializes objects and realized that this exactly how PHP is doing it. To inject a string, we were simply missing the length of the string!
We then simply tried to SQL inject some code. The open question, however, is, how does our query look like? A simple empty string solved this issue, since it returned an error when the following widget_tracker value was given.
The key seems to be within reach, but let's enumerate the existing tables first. Here, we need to know the number of columns our select query returns since the union will fail otherwise. We decided to simply brute-force it by starting with 1, and simply increasing the number.
Putting this into the PHP object, the value of widget_tracker base64 decoded looks like this:
Once we injected the union, we are getting the following tables and a few other ones that we do not care about (like COLLATIONS and other MySQL internal ones):
From there on, we can get the columns of the table
flag in a similar way:
which simply gives a single column:
The last step is now to simply get the rows of flag.
In the end, the following query gave us the flag:
In the heat of the competition we were using a huge mess of curl and Python to go through these steps and we also made a few interesting detours that led us to explore other tables and what other teams were doing. Since this code would not be straight-forward to understand or use, the following Python code unifies all those steps and should be pretty readable. It should only be necessary to add the PHPSESSID and change the injection variable to use it.