I heard you liked zip codes! Connect via nc c1.easyctf.com 12483 to prove your zip code knowledge.
Connecting to the server we receive some questions like this one:
1 2 3 4 5 6 7 8 9 10 11
+======================================================================+ | Welcome to Zippy! We love US zip codes, so we'll be asking you some | | simple facts about them, based on the 2010 Census. Only the | | brightest zip-code fanatics among you will be able to succeed! | | You'll have 30 seconds to answer 50 questions correctly. | +======================================================================+
3... 2... 1... Go!
Round 1 / 50 What is the water area (m^2) of the zip code 49446?
There are only 4 types of question.
I noted that when you send a wrong answer, the server gives you the right answer and closes the connection.
My first idea was to answer wrong stuff, and then store the right answer sent by the server in a SQLite database. When having the right answer in the database, sending it, and when not, sending random stuff to get and store the right answer.
So I made a ruby script to achieve that:
while flag == false s = TCPSocket.open(hostname, port) while chunck = s.read(1) print chunck raw += chunck
if /What .*\?/.match?(raw) # Extract zipcode and question type zipcode = raw.match(/([0-9]{5})/).captures[0] puts "\nMatched zipcode: #{zipcode}".colorize(:magenta) question_type = raw.match(/(latitude|land|longitude|water)/).captures[0] puts "Matched type: #{question_type}".colorize(:magenta)
# Check if in the database ans = db.execute("SELECT #{question_type} FROM zipcode WHERE zipcode = '#{zipcode}'") ans = ans[0] unless ans.nil? puts "Matched answer in database: #{ans}".colorize(:magenta) if ans.nil? # not found # send bad stuff s.puts 'bad' else # found s.puts ans puts ans end raw = '' elsif /The correct answer was ([0-9]+|\-{0,1}[0-9]+\.{1}[0-9]+)\.\n/.match?(raw) # get the good answer ans = raw.match(/The correct answer was ([0-9]+|\-{0,1}[0-9]+\.{1}[0-9]+)\.\n/).captures[0] puts "Matched answer: #{ans}".colorize(:magenta) # and store it if db.execute("SELECT zipcode FROM zipcode WHERE zipcode = '#{zipcode}'").empty? # new row db.execute("INSERT INTO zipcode (zipcode, #{question_type}) VALUES ('#{zipcode}', '#{ans}')") else # update db.execute("UPDATE zipcode SET #{question_type} = '#{ans}' WHERE zipcode = '#{zipcode}'") end raw = '' s.close break end end end
db.close
The script was perfectly working but that was far too long because of several issues:
each wrong answer close the connection so you loose time opening a new one
waiting for 3... 2... 1... Go!
there are thousands of zip code and 4 possible data values for each
Another idea I had before beginning my script was to use a web API but those are rather limited and never contains the wanted information.
So I read the server header again and I saw this: based on the 2010 Census. Using my web browser I found the U.S. Gazetteer Files that is The U.S. Gazetteer Files provide a listing of all geographic areas for selected geographic area types. The files include geographic identifier codes, names, area measurements, and representative latitude and longitude coordinates..
So I downloaded the 2010 ZIP Code Tabulation Areas file and looked at it:
s = TCPSocket.open(hostname, port) while chunck = s.read(1) print chunck raw += chunck
if /What .*\?/.match?(raw) # Extract zipcode and question type zipcode = raw.match(/([0-9]{5})/).captures[0] puts "\nMatched zipcode: #{zipcode}".colorize(:magenta) question_type = raw.match(/(latitude|land|longitude|water)/).captures[0] puts "Matched type: #{question_type}".colorize(:magenta)
# Find answer in the census File.open('2010_Gaz_zcta_national.txt', "r") do |fh| fh.readline # skip header # GEOID POP10 HU10 ALAND AWATER ALAND_SQMI AWATER_SQMI INTPTLAT INTPTLONG
while(line = fh.gets) != nil data = line.split if data[0] == zipcode answer = case question_type when'latitude'then data[7] # INTPTLAT when'longitude'then data[8] # INTPTLONG when'land'then data[3] # ALAND when'water'then data[4] # AWATER end s.puts answer puts "Answer sent: #{answer}".colorize(:magenta) break end end end raw = '' end end
s.close
And of course this time I got the flag quicker:
1 2
You succeeded! Here's the flag: easyctf{hope_you_liked_parsing_tsvs!}
I don't like it when people try to view source on my page. Especially when I put all this effort to put my flag verbatim into the source code, but then people just look at the source to find the flag! How annoying.
This time, when I write my wonderful website, I'll have to hide my beautiful flag to prevent you CTFers from stealing it, dagnabbit. We'll see what you're able to find...
Looking at the source code, we can see a script inside <script></script>:
functionprocess(a, b) { 'use strict'; var len = Math.max(a.length, b.length); var out = []; for (var i = 0, ca, cb; i < len; i++) { ca = a.charCodeAt(i % a.length); cb = b.charCodeAt(i % b.length); out.push(ca ^ cb); } returnString.fromCharCode.apply(null, out); }
(function (global) { 'use strict'; var formEl = document.getElementById('flag-form'); var inputEl = document.getElementById('flag'); var flag = 'Fg4GCRoHCQ4TFh0IBxENAE4qEgwHMBsfDiwJRQImHV8GQAwBDEYvV11BCA=='; formEl.addEventListener('submit', function (e) { e.preventDefault(); if (btoa(process(inputEl.value, global.encryptionKey)) === flag) { alert('Your flag is correct!'); } else { alert('Incorrect, try again.'); } }); })(window);
process(a, b) is just a xor function and flag is the encrypted (xored) flag. The xor key is global.encryptionKey so this is window.encryptionKey that is available in the browser.
I opened Firefox Web Developer toolbar and switched to the Console tab. Then it was easy to reverse the process:
1 2 3 4 5 6 7 8
> window.encryptionKey "soupy"
> var enc_flag = 'Fg4GCRoHCQ4TFh0IBxENAE4qEgwHMBsfDiwJRQImHV8GQAwBDEYvV11BCA=='; undefined