Fork me on GitHub

Hack.lu - Dalton's Corporate Security Safe

1

Myself along with a few of the other OverflowSecurity CTF team members participated in the Hack.lu CTF that just passed, and despite it being a very challenging CTF, we pulled 84th place out of 400 participating teams! Anyhow, I took on the Web challenge "Dalton's Corporate Security Safe", and had a lot of fun figuring this one out. Let's get into it!

The link took me to another page (Figure 1) with what appeared to be a CAPTCHA mechanism and a form field to enter in the code. I manually entered in the CAPTCHA values to get a feel for the system and discovered that (as anticipated) the CAPTCHA code changes from page to page as well as that if the code is not entered in a timely manner, I get kicked out of the cycle and have to start over again. Being able to programmatically submit the CAPTCHA code multiple times should do the trick.

2

Upon inspecting the page source, I realized this was not actually CAPTCHA code being generated, but some javascript designed to dynamically generate random alphanumeric values and write them to a HTML5 canvas element on the page. The javascript was in a nasty somewhat minified one-liner, so I expanded the code to make it more readable:

<script>
    var m = c.getContext('2d');
    var k = atob('Ng==');
    var u = m.createLinearGradient(0, 0, c.width, 0);
    u.addColorStop('0', '#1dfcdd');
    u.addColorStop('1.0', '#466c07');
    m.fillStyle = u;
    m.font = 'italic 13px geneva';
    m.fillText(k, 15, 17);
    var l = m.createLinearGradient(0, 0, c.width, 0);
    l.addColorStop('0', '#5a17b7');
    l.addColorStop('1.0', '#4e70cd');
    m.fillStyle = l;
    var a = /e/.source;
    m.font = ' 10px Gerogia';
    m.fillText(a, 42, 16);
    var n = m.createLinearGradient(0, 0, c.width, 0);
    n.addColorStop('0', '#f094f2');
    n.addColorStop('1.0', '#4bb189');
    m.fillStyle = n;
    var g = (5).toString(36);
    m.font = 'bold 13px verdana';
    m.fillText(g, 62, 18);
    var r = m.createLinearGradient(0, 0, c.width, 0);
    r.addColorStop('0', '#140917');
    r.addColorStop('1.0', '#4426a3');
    var v = /c/.source;
    m.fillStyle = r;
    m.font = ' 13px verdana';
    m.fillText(v, 72, 15);
    var d = m.createLinearGradient(0, 0, c.width, 0);
    d.addColorStop('0', '#8e313c');
    d.addColorStop('1.0', '#d93a5c');
    var b = atob('ZQ==');
    m.fillStyle = d;
    m.font = ' 16px Gerogia';
    m.fillText(b, 50, 18);
    var p = m.createLinearGradient(0, 0, c.width, 0);
    p.addColorStop('0', '#b93b0e');
    p.addColorStop('1.0', '#34af95');
    var b = String.fromCharCode(52);
    m.fillStyle = p;
    m.font = 'italic 15px serif';
    m.fillText(b, 34, 18);
    var o = m.createLinearGradient(0, 0, c.width, 0);
    o.addColorStop('0', '#83fe1a');
    o.addColorStop('1.0', '#d83248');
    m.fillStyle = o;
    var e = String.fromCharCode(101);
    m.font = ' 13px sans-serif';
    m.fillText(e, 4, 17);
    var s = m.createLinearGradient(0, 0, c.width, 0);
    s.addColorStop('0', '#2ec451');
    var h = ([][+[]] + "")[4];
    s.addColorStop('1.0', '#ef566e');
    m.fillStyle = s;
    m.font = 'bold 12px wingdinds';
    m.fillText(h, 22, 20);
</script>

Now that the javascript was much easier to read, it was clear what was happening. Each fillText() call was inserting a character on the HTML5 canvas element, with the X and Y coordinates being set, giving the characters it's ransom note-esque appearance. Tracing back from fillText() it became possible to see that the alphanumeric characters were being created by way of a few various methods. It took a few minutes to try and enumerate all possible methods:

var k = atob('Ng==')
var a = /e/.source
var e = String.fromCharCode(101)
var h = ([][+[]] + "")[4]
var g = (13).toString(36)
var m = ([] + {})[2]

Armed with this knowledge as well as knowing that I could determine the order of the characters via the X coordinate of the fillText() call, I could finally create something to automate the process of reading the code and submitting the form with the interpreted information. For this, I decided to use Splinter, a python library used to automate the testing of web applications. Splinter is able to launch a browser and read and control elements on the page and interact with forms and javascript so, while not exactly the most efficient solution, I always enjoy watching my python code control a browser! Here are the general steps I took with my script, which I'll also include at the end:

  • Load the page and parse the contents of the javascript.
  • Pull out the details of all the fillText() calls to identify which variables hold one of the possible alphanumeric character generation as well as the X coordinate of the character.
  • Identify the character generation lines and evaluate the javascript code dynamically to retrieve the actual character value.
  • In a loop, repeat the above steps, fill and submit the form.

After 10 successful cycles of the above steps, a new link was shown on the page indicating that we unlocked the Security Zone (Figure 2)! I added a step to my process to click the 'Security Zone' link if presented and re-ran my script. Clicking that link sent me to a page with the flag, which I recorded and added as part of my script output (Figure 3).

3

4

Flag: fef9565c97c3a62fe10d2a0084a9e8179d72f4a05084997cb80e900d1a77a42e3

Finally, you can check out my code which solved this challenge below. It wasn't cleaned up or optimized as I was in a rush to solve the challenge, but it should be pretty straightforward with the above steps listed out. Enjoy!

#!/usr/bin/env python

import time
import sys
import re
import base64
import execjs
import collections
from pprint import pprint
from splinter import Browser

def decipher_script_contents(browser):
    script_contents = browser.find_by_tag('script').first.html
    script_lines = script_contents.split(';')

    atob_pattern = re.compile(r'\'(.+)\'')
    pattern = re.compile(r'.+\[[0-9]+\]$')

    fill_text_records = []
    for line in script_lines:
        var = ''
        x, y = 0, 0
        if 'fillText' in line:
            match = re.search(r'.+\((\w),(\d+),(\d+)\)$',line)
            if match:
                var, x, y = match.groups()
            else:
                sys.exit(1)
            fill_text_records.append({
                'var': var,
                'x': x,
                'y': y,
            })

    for record in fill_text_records:
        var_definitions = [val for val in script_lines if "var %s=" % record['var'] in val]
        valid_var_definitions = []
        for var_definition in var_definitions:
            if 'fromCharCode' in var_definition or 'toString' in var_definition \
            or 'source' in var_definition or 'atob' in var_definition \
            or '[]+{}' in var_definition or pattern.match(var_definition):
                valid_var_definitions.append(var_definition)
        record.update({'eval': valid_var_definitions[0]})
        script_lines.remove(valid_var_definitions[0])

    final = {}
    for record in fill_text_records:
        record['eval'] = record['eval'].split('=',1)[1]
        if 'atob' in record['eval']:
            # execjs sucks and doesn't eval atob()
            line = atob_pattern.findall(record['eval'])[0]
            record['eval'] = str(base64.b64decode(line))
        else:
            record['eval'] = str(execjs.eval(record['eval']))
        final.update({int(record['x']):record['eval']})
    ordered = collections.OrderedDict(sorted(final.items()))
    return ''.join(ordered.values())

with Browser() as browser:
    browser.visit('https://wildwildweb.fluxfingers.net:1422/')

    for i in xrange(0,15):
        if 'Security Zone' in browser.html:
            browser.find_link_by_text('Security Zone').first.click()
            flag = browser.find_by_tag('body').first.text
            print flag
            break
        solution_value = decipher_script_contents(browser)
        print 'submitting solution: %s: ' % solution_value
        browser.fill('solution',solution_value)
        browser.find_by_value('OK').first.click()
print 'done, enjoy the flag!'

Comments