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.

Figure 1 – CAPTCHA-esque form (modified to be viewed easier)
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).

Figure 2 – Security Zone link discovered.

Figure 3 – Script results with the flag.
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!'