Fork me on GitHub - Dalton's Corporate Security Safe


Myself along with a few of the other OverflowSecurity CTF team members participated in the 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.


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:

    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);

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).



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 ='.+\((\w),(\d+),(\d+)\)$',line)
            if match:
                var, x, y = match.groups()
                '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):
        record.update({'eval': 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))
            record['eval'] = str(execjs.eval(record['eval']))
    ordered = collections.OrderedDict(sorted(final.items()))
    return ''.join(ordered.values())

with Browser() as browser:

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