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:

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/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:

1
2
3
4
5
6
7
8
9
10
11
$ 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/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:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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:

1
2
3
4
5
6
7
8
> window.encryptionKey
"soupy"

> var enc_flag = 'Fg4GCRoHCQ4TFh0IBxENAE4qEgwHMBsfDiwJRQImHV8GQAwBDEYvV11BCA==';
undefined

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