diff --git a/Gemfile b/Gemfile index 7276ba257..0f10e8512 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,7 @@ gem 'credy' gem 'pg' gem 'cinch' gem 'nori' +gem 'bcrypt' gem 'programr', :git => "http://github.com/robertjwhitney/programr.git" gem 'process_helper' gem 'ovirt-engine-sdk' @@ -27,4 +28,4 @@ group :test, :development do gem 'rake' gem 'rdoc' gem 'yard' -end \ No newline at end of file +end diff --git a/lib/output/ctfd_generator.rb b/lib/output/ctfd_generator.rb new file mode 100644 index 000000000..2f097aca0 --- /dev/null +++ b/lib/output/ctfd_generator.rb @@ -0,0 +1,477 @@ +require 'bcrypt' + +# Convert systems objects into a format that can be imported into CTFd +class CTFdGenerator + + POINTS_PER_FLAG = 100 + FREE_POINTS = 200 + + # How much of the total reward is offset by the cost of all the hints for that flag + # Since CTFd doesn't force hints to be taken in order, we penalise bigger hints much more, to the point that + # they need to think before taking a hint as they can't afford to take all of them + PERCENTAGE_COST_FOR_ALL_HINTS = 0.8 # 80 / number of hints (normal nudge hints are cheap) + PERCENTAGE_COST_FOR_BIG_HINTS = 0.5 # 50% cost for a big hint (bigger hints are less so) + PERCENTAGE_COST_FOR_REALLY_BIG_HINTS = 0.7 # 50% cost for a really big hint (the name of the SecGen module) + PERCENTAGE_COST_FOR_SOLUTION_HINTS = 0.8 # 80% cost for a solution (msf exploit, etc) + + + # @param [Object] systems the list of systems + # @param [Object] scenario the scenario file used to generate + # @param [Object] time the current time as a string + def initialize(systems, scenario, time) + @systems = systems + @scenario = scenario + @time = time + end + + # outputs a hash of filenames with JSON contents + # @return [Object] hash of files + def ctfd_files + + challenges = [] + hints = [] + keys = [] + + challenges << { + "id"=> 1, + "name"=>"Free points", + "description"=>"Some free points to get you started (and for purchasing hints)!\n Enter flag{FREEPOINTS}", + "max_attempts"=>0, + "value"=>FREE_POINTS, + "category"=>"Freebie", + "type"=>"standard", + "hidden"=>0} + keys << { + "id"=>1, + "chal"=>1, + "type"=>"static", + "flag"=>"flag{FREEPOINTS}", + "data"=>nil} + + @systems.each { |system| + system.module_selections.each { |selected_module| + # start by finding a flag, and work the way back providing hints + selected_module.output.each { |output_value| + if output_value.match(/^flag{.*$/) + challenge_id = challenges.length + 1 + challenges << { + "id"=> challenge_id, + "name"=>"", + "description"=>"Remember, search for text in the format of flag{SOMETHING}, and submit it for points. If you are stuck a hint may help!", + "max_attempts"=>0, + "value"=>POINTS_PER_FLAG, + "category"=>"#{system.name} VM (#{system.module_selections.first.attributes['platform'].first})", + "type"=>"standard", + "hidden"=>0} + key_id = keys.length + 1 + keys << { + "id"=>key_id, + "chal"=>challenge_id, + "type"=>"static", + "flag"=>output_value, + "data"=>nil} + + collected_hints = [] + system.module_selections.each { |search_module_for_hints| + if search_module_for_hints.unique_id == selected_module.write_to_module_with_id + collected_hints = get_module_hints(search_module_for_hints, collected_hints, system.module_selections) + end + } + + collected_hints.each { |collected_hint| + hint_id = hints.length + 1 + # weight hints for big_hint + if collected_hint["hint_type"] == "solution" + cost=(POINTS_PER_FLAG * PERCENTAGE_COST_FOR_SOLUTION_HINTS).round + elsif collected_hint["hint_type"] == "really_big_hint" + cost=(POINTS_PER_FLAG * PERCENTAGE_COST_FOR_REALLY_BIG_HINTS).round + elsif collected_hint["hint_type"] == "big_hint" + cost=(POINTS_PER_FLAG * PERCENTAGE_COST_FOR_BIG_HINTS).round + else + cost=(POINTS_PER_FLAG * PERCENTAGE_COST_FOR_ALL_HINTS / collected_hints.length).round + end + hints << { + "id"=> hint_id, + "type"=>0, + "chal"=>challenge_id, + "hint"=>collected_hint["hint_text"], + "cost"=>cost + } + } + end + } + } + } + + output_hash = { + "alembic_version.json" => "", + "awards.json" => "", + "challenges.json" => challenges_json(challenges), + "config.json" => config_json(), + "files.json" => files_json(), + "hints.json" => hints_json(hints), + "keys.json" => keys_json(keys), + "pages.json" => pages_json(), + "solves.json" => "", + "tags.json" => "", + "teams.json" => teams_json(), + "tracking.json" => "", + "unlocks.json" => "", + "wrong_keys.json" => "", + } + + output_hash + + end + + def files_json + return '' + end + + def challenges_json(challenges) + {"count"=>challenges.length, + "results"=>challenges, + "meta"=>{} + }.to_json + end + + def config_json + config_json_hash = { + "count" => 31, + "results" => [ + { + "id"=>1, + "key"=>"next_update_check", + "value"=>"1529096764" + }, + { + "id"=>2, + "key"=>"ctf_version", + "value"=>"1.2.0" + }, + { + "id"=>3, + "key"=>"ctf_theme", + "value"=>"core" + }, + { + "id"=>4, + "key"=>"ctf_name", + "value"=>"SecGenCTF" + }, + { + "id"=>5, + "key"=>"ctf_logo", + "value"=>nil #"fca9b07e1f3699e07870b86061815b1c/logo.svg" + }, + { + "id"=>6, + "key"=>"workshop_mode", + "value"=>"0" + }, + { + "id"=>7, + "key"=>"hide_scores", + "value"=>"0" + }, + { + "id"=>8, + "key"=>"prevent_registration", + "value"=>"0" + }, + { + "id"=>9, + "key"=>"start", + "value"=>nil + }, + { + "id"=>10, + "key"=>"max_tries", + "value"=>"0" + }, + { + "id"=>11, + "key"=>"end", + "value"=>nil + }, + { + "id"=>12, + "key"=>"freeze", + "value"=>nil + }, + { + "id"=>13, + "key"=>"view_challenges_unregistered", + "value"=>"0" + }, + { + "id"=>14, + "key"=>"verify_emails", + "value"=>"0" + }, + { + "id"=>15, + "key"=>"mail_server", + "value"=>nil + }, + { + "id"=>16, + "key"=>"mail_port", + "value"=>nil + }, + { + "id"=>17, + "key"=>"mail_tls", + "value"=>"0" + }, + { + "id"=>18, + "key"=>"mail_ssl", + "value"=>"0" + }, + { + "id"=>19, + "key"=>"mail_username", + "value"=>nil + }, + { + "id"=>20, + "key"=>"mail_password", + "value"=>nil + }, + { + "id"=>21, + "key"=>"mail_useauth", + "value"=>"0" + }, + { + "id"=>22, + "key"=>"setup", + "value"=>"1" + }, + { + "id"=>23, + "key"=>"css", + "value"=>File.read(ROOT_DIR + '/lib/templates/CTFd/css.css') + }, + { + "id"=>24, + "key"=>"view_scoreboard_if_authed", + "value"=>"0" + }, + { + "id"=>25, + "key"=>"prevent_name_change", + "value"=>"1" + }, + { + "id"=>26, + "key"=>"version_latest", + "value"=>nil + }, + { + "id"=>27, + "key"=>"mailfrom_addr", + "value"=>nil + }, + { + "id"=>28, + "key"=>"mg_api_key", + "value"=>nil + }, + { + "id"=>29, + "key"=>"mg_base_url", + "value"=>nil + }, + { + "id"=>30, + "key"=>"view_after_ctf", + "value"=>"1" + }, + { + "id"=>31, + "key"=>"paused", + "value"=>"0" + } + ], + "meta"=>{} + } + + config_json_hash.to_json + end + + def hints_json(hints) + {"count"=>hints.length, + "results"=>hints, + "meta"=>{} + }.to_json + end + + def keys_json(keys) + {"count"=>keys.length, + "results"=>keys, + "meta"=>{} + }.to_json + end + + def pages_json + pages_json_hash = { + "count" => 2, + "results" => [ + { + "id"=>1, + "route"=>"index", + "html"=>File.read(ROOT_DIR + '/lib/templates/CTFd/index.html'), + "auth_required"=>0, + "draft"=>0, + "title"=>"Welcome" + }, + { + "id"=>2, + "route"=>"submit", + "html"=>File.read(ROOT_DIR + '/lib/templates/CTFd/submit.html'), + "auth_required"=>0, + "draft"=>0, + "title"=>"Flag submission" + } + ], + "meta"=>{} + } + + pages_json_hash.to_json + end + + def teams_json + + teams_json_hash = { + "count" => 2, + "results" => [ + { + "id"=>1, + "name"=>"adminusername", + "email"=>"admin@email.com", + "password"=>password_hash_string("adminpassword"), + "website"=>nil, + "affiliation"=>nil, + "country"=>nil, + "bracket"=>nil, + "banned"=>0, + "verified"=>1, + "admin"=>1, + "joined"=>"2018-06-22T10:46:26" + }, + { + "id"=>2, + "name"=>"Me", + "email"=>"email@email.com", + "password"=>password_hash_string("mypassword"), + "website"=>nil, + "affiliation"=>nil, + "country"=>nil, + "bracket"=>nil, + "banned"=>0, + "verified"=>1, + "admin"=>1, + "joined"=>"2018-06-22T10:46:26" + } + ], + "meta"=>{} + } + + teams_json_hash.to_json + + end + + # fix difference between ruby and python bcrypt formats used by libraries + # $bcrypt-sha256$variant,rounds$salt$checksum + # python lib used by CTFd expects , between variant and rounds, the ruby lib puts a $ there... + def password_hash_string(pass) + hash_string = "$bcrypt-sha256" + BCrypt::Password.create(pass) + hash_string[17]= "," + hash_string + end + + def get_module_hints(search_module_for_hints, collected_hints, all_module_selections) + + if search_module_for_hints.write_to_module_with_id != "" + # recursion -- show hints for any parent modules + all_module_selections.each { |search_module_for_hints_recursive| + if search_module_for_hints_recursive.unique_id == search_module_for_hints.write_to_module_with_id + get_module_hints(search_module_for_hints_recursive, collected_hints, all_module_selections) + end + } + end + + case search_module_for_hints.module_type + when "vulnerability" + case search_module_for_hints.attributes['access'].first + when "remote" + collected_hints = collect_hint("A vulnerability that can be accessed/exploited remotely. Perhaps try scanning the system/network?", "#{search_module_for_hints.unique_id}remote", "normal", collected_hints) + when "local" + collected_hints = collect_hint("A vulnerability that can only be accessed/exploited with local access. You need to first find a way in...", "#{search_module_for_hints.unique_id}local", "normal", collected_hints) + end + type = search_module_for_hints.attributes['type'].first + unless type == 'system' or type == 'misc' or type == 'ctf' or type == 'local' or type == 'ctf_challenge' + collected_hints = collect_hint("The system is vulnerable in terms of its #{search_module_for_hints.attributes['type'].first}", "#{search_module_for_hints.unique_id}firsttype", "big_hint", collected_hints) + end + collected_hints = collect_hint("The system is vulnerable to #{search_module_for_hints.attributes['name'].first}", "#{search_module_for_hints.unique_id}name", "really_big_hint", collected_hints) + if search_module_for_hints.attributes['hint'] + search_module_for_hints.attributes['hint'].each_with_index { |hint, i| + collected_hints = collect_hint(clean_hint(hint), "#{search_module_for_hints.unique_id}hint#{i}", "big_hint", collected_hints) # .gsub(/\s+/, ' ') + } + end + if search_module_for_hints.attributes['solution'] + solution = search_module_for_hints.attributes['solution'].first + collected_hints = collect_hint(clean_hint(solution), "#{search_module_for_hints.unique_id}solution", "solution", collected_hints) + end + if search_module_for_hints.attributes['msf_module'] + collected_hints = collect_hint("Can be exploited using the Metasploit module: #{search_module_for_hints.attributes['msf_module'].first}", "#{search_module_for_hints.unique_id}msf_module", "big_hint", collected_hints) + end + + when "service" + collected_hints = collect_hint("The flag is hosted using #{search_module_for_hints.attributes['type'].first}", "#{search_module_for_hints.unique_id}type", "normal", collected_hints) + when "encoder" + collected_hints = collect_hint("The flag is encoded/hidden somewhere", "#{search_module_for_hints.unique_id}itsanencoder", "normal", collected_hints) + if search_module_for_hints.attributes['type'].include? 'string_encoder' + collected_hints = collect_hint("There is a layer of encoding using a standard encoding method, look for an unusual string of text and try to figure out how it was encoded, and decode it", "#{search_module_for_hints.unique_id}stringencoder", "normal", collected_hints) + end + if search_module_for_hints.attributes['solution'] == nil + collected_hints = collect_hint("The flag is encoded using a #{search_module_for_hints.attributes['name'].first}", "#{search_module_for_hints.unique_id}name", "really_big_hint", collected_hints) + end + if search_module_for_hints.attributes['hint'] + search_module_for_hints.attributes['hint'].each_with_index { |hint, i| + collected_hints = collect_hint(clean_hint(hint), "#{search_module_for_hints.unique_id}hint#{i}", "big_hint", collected_hints) + } + end + if search_module_for_hints.attributes['solution'] + solution = search_module_for_hints.attributes['solution'].first + collected_hints = collect_hint(clean_hint(solution), "#{search_module_for_hints.unique_id}solution", "solution", collected_hints) + end + when "generator" + if search_module_for_hints.attributes['hint'] + search_module_for_hints.attributes['hint'].each_with_index { |hint, i| + collected_hints = collect_hint(clean_hint(hint), "#{search_module_for_hints.unique_id}hint#{i}", "big_hint", collected_hints) + } + end + if search_module_for_hints.attributes['solution'] + solution = search_module_for_hints.attributes['solution'].first + collected_hints = collect_hint(clean_hint(solution), "#{search_module_for_hints.unique_id}solution", "solution", collected_hints) + end + end + + collected_hints + end +end + +def collect_hint(hint_text, hint_id, hint_type, collected_hints) + collected_hints << { + "hint_text"=>hint_text, + "hint_type"=>hint_type, + "hint_id"=>hint_id + } +end + +def clean_hint str + str.tr("\n",'').gsub(/\s+/, ' ') +end diff --git a/lib/output/project_files_creator.rb b/lib/output/project_files_creator.rb index 9e22bb337..f427fe7bf 100644 --- a/lib/output/project_files_creator.rb +++ b/lib/output/project_files_creator.rb @@ -2,8 +2,10 @@ require 'erb' require_relative '../helpers/constants.rb' require_relative 'xml_scenario_generator.rb' require_relative 'xml_marker_generator.rb' +require_relative 'ctfd_generator.rb' require 'fileutils' require 'librarian' +require 'zip/zip' class ProjectFilesCreator # Creates project directory, uses .erb files to create a report and the vagrant file that will be used @@ -83,7 +85,7 @@ class ProjectFilesCreator if File.file? packerfile_path Print.info "Would you like to use the packerfile to create the packerfile from the given url (y/n)" - # TODO: remove user interaction, this should be an config option + # TODO: remove user interaction, this should be set via a config option (Print.info "Exiting as vagrant needs the basebox to continue"; abort) unless ['y','yes'].include?(STDIN.gets.chomp.downcase) Print.std "Packerfile #{packerfile_path.split('/').last} found, building basebox #{url.split('/').last} via packer" @@ -140,6 +142,37 @@ class ProjectFilesCreator Print.err "Error writing file: #{e.message}" abort end + + # Create the CTFd zip file for import + ctfdfile = "#{@out_dir}/CTFd_importable.zip" + Print.std "Creating CTFd configuration: #{ctfdfile}" + + ctfd_generator = CTFdGenerator.new(@systems, @scenario, @time) + ctfd_files = ctfd_generator.ctfd_files + + # zip up the CTFd export + begin + Zip::ZipFile.open(ctfdfile, Zip::ZipFile::CREATE) { |zipfile| + zipfile.mkdir("db") + ctfd_files.each do |ctfd_file_name, ctfd_file_content| + zipfile.get_output_stream("db/#{ctfd_file_name}") { |f| + f.print ctfd_file_content + } + end + zipfile.mkdir("uploads") + # TODO: could add a logo image + # zipfile.mkdir("uploads/uploads") # empty as in examples + # zipfile.mkdir("uploads/fca9b07e1f3699e07870b86061815b1c") + # zipfile.get_output_stream("uploads/fca9b07e1f3699e07870b86061815b1c/logo.svg") { |f| + # f.print File.readlines(ROOT_DIR + '/lib/resources/images/svg_icons/flag.svg') + # } + } + rescue StandardError => e + Print.err "Error writing zip file: #{e.message}" + abort + end + + Print.std "VM(s) can be built using 'vagrant up' in #{@out_dir}" end diff --git a/lib/resources/images/svg_icons/flag.svg b/lib/resources/images/svg_icons/flag.svg new file mode 100644 index 000000000..f49415035 --- /dev/null +++ b/lib/resources/images/svg_icons/flag.svg @@ -0,0 +1,61 @@ + + + + + + image/svg+xml + + + + + + + + + + Layer 1 + + + diff --git a/lib/templates/CTFd/css.css b/lib/templates/CTFd/css.css new file mode 100644 index 000000000..e964d0966 --- /dev/null +++ b/lib/templates/CTFd/css.css @@ -0,0 +1,57 @@ +img.ctf_logo { + /* from black icon to light grey */ + filter=> invert(0.8) sepia(1) saturate(0) hue-rotate(0deg); +} + +.challenge-button{ + +} +.corner-button-check{ + margin-right: 3px !important; +} +.challenges-row{ + display: inline-block; +} +.category-challenges { + display: inline-block; + width: auto; + max-width: 450px; + border: 5px; + border-style: solid; + border-color: #ddd; + border-radius: 7px; + padding: 5px; + margin: 5px; + background: linear-gradient(whitesmoke, white); +} +.category-challenges:hover { + border-color: #aaa; +} + +.category-challenges::after{ + height: 5px; + width: 40px; + background-color: #ddd; + content: " "; + position: absolute; + bottom: -10px; + left: calc(50% - 20px); + margin-left: -5px; +} +.pt-5{ + display: inline-block; +} +.col-md-3{ + width: auto; + padding-right: 5px; + padding-left: 5px; + max-width: 450px !important; + vertical-align: bottom; +} +h3{ + font-size: 1rem; +} +.jumbotron { + margin-left: -100%; + margin-right: -100%; +} diff --git a/lib/templates/CTFd/index.html b/lib/templates/CTFd/index.html new file mode 100644 index 000000000..d20ec03a8 --- /dev/null +++ b/lib/templates/CTFd/index.html @@ -0,0 +1,13 @@ +
+
+
+

Welcome to SecGenCTF

+
+

+ Here you can submit flags you discover, and review the challenges, and (if available) purchase hints. +

+

+ Good luck! +

+
+
diff --git a/lib/templates/CTFd/submit.html b/lib/templates/CTFd/submit.html new file mode 100644 index 000000000..5392e35f6 --- /dev/null +++ b/lib/templates/CTFd/submit.html @@ -0,0 +1,128 @@ +
+
+

Flag submission

+
+
+ + + + +