EasyCTF IV - Write-ups

Information#

Version#

By Version Comment
noraj 1.0 Creation

CTF#

80 - Zippity - Misc#

Written by gengkev

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:

+======================================================================+
| 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:

#!/usr/bin/ruby

require 'socket'
require 'sqlite3'
require 'colorize'

hostname = 'c1.easyctf.com'
port = 12483

raw = ''
flag = false

db = SQLite3::Database.new "zipcode.db"

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:

$ head 2010_Gaz_zcta_national.txt
GEOID	POP10	HU10	ALAND	AWATER	ALAND_SQMI	AWATER_SQMI	INTPTLAT	INTPTLONG
00601	18570	7744	166659789	799296	      64.348	       0.309	 18.180555	 -66.749961
00602	41520	18073	79288158	4446273	      30.613	       1.717	 18.362268	 -67.176130
00603	54689	25653	81880442	183425	      31.614	       0.071	 18.455183	 -67.119887
00606	6615	2877	109580061	12487	      42.309	       0.005	 18.158345	 -66.932911
00610	29016	12618	93021467	4172001	      35.916	       1.611	 18.290955	 -67.125868
00612	67010	30992	175106243	9809163	      67.609	       3.787	 18.402239	 -66.711400
00616	11017	4896	29870473	149147	      11.533	       0.058	 18.420412	 -66.671979
00617	24597	10594	39347158	3987969	      15.192	       1.540	 18.445147	 -66.559696
00622	7853	8714	75077028	1694917	      28.987	       0.654	 17.991245	 -67.153993

I was pretty sure the author of the challenge was using this file too so I wrote a new ruby script again:

#!/usr/bin/ruby

require 'socket'
require 'colorize'

hostname = 'c1.easyctf.com'
port = 12483

raw = ''
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)

        # 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:

You succeeded! Here's the flag:
easyctf{hope_you_liked_parsing_tsvs!}

80 - Nosource, Jr. - Web#

Written by gengkev

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>:

function process(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);
  }
  return String.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:

> window.encryptionKey
"soupy"

> var enc_flag = 'Fg4GCRoHCQ4TFh0IBxENAE4qEgwHMBsfDiwJRQImHV8GQAwBDEYvV11BCA==';
undefined

> process(atob(enc_flag), window.encryptionKey);
"easyctf{congrats!_but_now_f0r_n0s0urc3_...}"
Share