# A7 ~ Gee cue elle (misc, 283p)
> We installed a fancy automatic attack protection system to be more secure against automated attacks from robots and hackers so it can be fully A7 compliant!
> Hint: .yaml~
> start.html
In this task we are given a small HTML file. Its whole content is here:```html<script>location.replace('http://'+parseInt(Math.random()*1e15)+'-abuse.web.ctfcompetition.com/login');</script>```
It's a simple redirect to randomly generated host. What we see next, is a simple login site. There is a login and passwordfield, along with a `~Login~` button. Trying to type anything into the fields and clicking thebutton shows a popup saying `Please match the requested format`. OK, we may be able to craft arequest anyway (the check is client-side), but let's not do this yet. Instead, we'll peekinto the source of the website. Honestly, not much is here, just the regex for checking the fields: `admin` for the login field, and `CTF[{]qu0t45[a-z0-9_]{16}www-[0-9A-Za-z_-]{64}[}]` forthe password.
When we type the login and password matching the formats, we receive a popup saying `Wrong password`. Nothing unexpected. However, if we check for SQL injection by forging a request with apostrophe character in password field, we receive a redirect to a subpageshowing popup `Parse Error: Identifier is a reserved keyword at symbol ANCESTOR`.Hmmm. Googling the error shows it's an error of GQL (Google Query Language, alternative to SQL).Now we can make sense of the challenge title: "Gee cue elle" is the phonetic form of GQL.
GQL is known for being difficult to inject to. We were stuck at this point for a while, but then a hint arrived: `.yaml~`.
As one of the first steps, we checked `robots.txt` file. It showed:```User-agent: *Disallow: /app.yamlDisallow: qa!```We obviously tried to fetch the `app.yaml`, but the request was blocked (maybe permissions issue). Anyway, after the hint, we thought to append a tilde character to the filename, as though someone left editor temporary files on the server. And it worked! Here are the contentsof the file:```yamlservice: anon2-lmuwucba5we9gi5aruntime: python27api_version: 1threadsafe: true
handlers:- url: /login  script: main.app
- url: /  static_files: index.html  upload: index.html
- url: /(.+)  static_files: \1  upload: .*[.](?!py$).*
libraries:- name: webapp2  version: latest- name: jinja2  version: latest
skip_files:- ^(.*/)?\.bak$```
Looks like it's App Engine's configuration file (`https://cloud.google.com/appengine/docs/standard/python/config/appref`).After looking at some of sample projects using this platform, we noticed a lot of them contains`main.py` file. We checked for its presence on the server, and we managed to download itwhen we appended a tilde (so, temporary file again).
Now we have full application source code. It appears it's a pretty basic login checker,but just as we noticed earlier, there is an GQL injection:```python  def post(self):    sql = "SELECT password FROM UserModel WHERE ANCESTOR IS :1 AND user = '%s'"    query = ndb.gql(sql % self.request.get("user"), self.quota.key)    result = query.fetch(1)    if not result:      self.redirect("/index.html?e=%s" % urllib.quote("Wrong username"))    elif result[0].password != self.request.get("password"):      raise Exception("Wrong password")    else:      self.response.write(self.request.get("password"))```The query should have used `:2` placeholder for username, instead of formatting the queryusing `%s`. 
Well, it seems we can't really get password directly sent to us, because the only resultwe can have is that we either have `Wrong username` error, or `Wrong password` one (orcorrect password, but we won't get it by accident ;]). Wrong username will be returned onlyif the query doesn't return any rows - and we know `admin` user exists. So, if we setusername to `admin' AND some_check`, we might have error-based injection, and be able to dump the password bit by bit. In fact, we used the following username:```admin' AND password > 'checked_password```The full query then looks like:```sqlSELECT password FROM UserModel WHERE ANCESTOR IS :1 AND user = 'admin' AND password > 'checked_password'```Now we can binary searched the password.
... or so we thought. I haven't mentioned that, but the application implements a simpleabuse detection. When too many password checks have been done, or too many errors have been made, it bans us for 90s. In addition, it bans us permamently after 2240s. It even writes that in the response if we get banned:```html<h1>Abuse detection system triggered!</h1><h3>You have been banned for 90 seconds.</h3>     If you believe this is a mistake, contact your system administrator.   Possible reasons include:  Generating too many errors too quickly  Making too many requests too quickly  Spending too much time without authenticating  ```
     If you believe this is a mistake, contact your system administrator.   Possible reasons include: 

When I first implemented the binary search, I just got throttled so much, that I eventuallyhit the 2240s limit while getting less than half of the password. I retried a couple of times,but the results were the same.
Hmmm. Let's make some calculations.
The password follows a certain regex. From the Python source, we know all the characters,except for 64 pseudo-random base64 characters. Since each base64 character has 6 bits ofentropy, that means we have a total of `64*6 == 384` bits and we need that many queries.On average, half of them counts as errors (`Wrong password` is signalled through raisingan exception, while `Wrong username` directly redirects). If we don't throttle requestson our side, we will get banned for 90s after around 4 requests, so reading all bits willtake approximately `384/4*90 == 8640` seconds. That's way too much. We can reduce itthreefold if we throttle the requests locally - in other words, when we got 2 errors withinthe last 30 seconds, we wait until this condition is no longer true. That algorithmwill give password after, very approximately, `384/4*30 == 2880` seconds. That's muchcloser to the limit of 2240s, but still, when we tried it a couple of times, we weregetting banned too soon. 
What else can we do? Well, let's assume for a moment only error requests are counted towardsthe limit, and otherwise we can do them as much as we can. Then, we would be able to checkconsecutive letters: `A`, `B`, `C`, and so on, each time getting free `Wrong username`,until we finally hit the correct letter with `Wrong password`. This would take only oneerror per letter, so one timeout per 4 letters or `64/4*30 == 480` seconds (notcounting latency for checking all the small characters). 
Unfortunately, the non-error requests are limited too (up to 13 requests per 30 seconds),so we cannot directly apply that strategy. Instead, we biased our binary search algorithmto divide the search space not in the middle, but at around 90%. The value is pretty arbitrary,it should probably be close to `allowed_hits/(allowed_hits+allowed_errs)`, but the trulyoptimal strategy would also take into account how many of the errors are already used up.We decided not to bother with such hard optimizations and just opened 20 instances of thebiased binary search, and after 2240s we found two of them actually managed to squeezethrough and calculated the whole flag.
CTF-quality code for solving is available in `doit2.py` file. 