Merge pull request #134 from Th3Prim3/master

CTFd Export/Import Fix (fixes #129)
This commit is contained in:
Cliffe
2019-03-27 20:19:04 +00:00
committed by GitHub
5 changed files with 255 additions and 254 deletions

View File

@@ -40,8 +40,7 @@ Install all the required packages:
wget https://releases.hashicorp.com/vagrant/1.9.8/vagrant_1.9.8_x86_64.deb
sudo apt install ./vagrant_1.9.8_x86_64.deb
# install other required packages via repos
sudo apt-get install ruby-dev zlib1g-dev liblzma-dev build-essential patch virtualbox ruby-bundler imagemagick libmagickwand-dev exiftool libpq-dev libcurl4-openssl-dev libxml2-dev graphviz graphviz-dev
```
sudo apt-get install ruby-dev zlib1g-dev liblzma-dev build-essential patch virtualbox ruby-bundler imagemagick libmagickwand-dev exiftool libpq-dev libcurl4-openssl-dev libxml2-dev graphviz graphviz-dev python3.6 python3-passlib
Copy SecGen to a directory of your choosing, such as */home/user/bin/SecGen*
@@ -117,7 +116,7 @@ Scenarios can be found in the scenarios/ directory. For example, to spin up a VM
```bash
ruby secgen.rb --scenario scenarios/examples/remotely_exploitable_user_vulnerability.xml run
```
![gify goodness](lib/resources/images/readme_gifs/secgen_random_example.gif "Remotly exploitable example where an attacker ends up with user-level access")
![gify goodness](lib/resources/images/readme_gifs/secgen_random_example.gif "Remotely exploitable example where an attacker ends up with user-level access")
#### VMs for a security audit of an organisation
To generate a set of VMs for a randomly generated fictional organisation, with a desktop system, webserver, and intranet server:
@@ -132,6 +131,11 @@ To generate a set of VMs for a CTF competition:
ruby secgen.rb --scenario scenarios/ctf/flawed_fortress_1.xml run
```
Note that a 'CTFd_importable.zip' file is also generated, containing all the flags and hints, which you can import into the [CTFd scoreboard frontend](https://github.com/CTFd/CTFd).
This is compatible with CTFd v2.0.2 and newer.
**Default admin account:**
Username: adminusername
Password: adminpassword
### Defining new scenarios
Writing your own scenarios enables you to define a VM or set of VMs with a configuration as specific or general as desired.

View File

@@ -1,5 +1,3 @@
require 'bcrypt'
# Convert systems objects into a format that can be imported into CTFd
class CTFdGenerator
@@ -30,7 +28,7 @@ class CTFdGenerator
challenges = []
hints = []
keys = []
flags = []
challenges << {
"id"=> 1,
@@ -40,12 +38,13 @@ class CTFdGenerator
"value"=>FREE_POINTS,
"category"=>"Freebie",
"type"=>"standard",
"hidden"=>0}
keys << {
"state"=>"visible",
"requirements"=>"null"}
flags << {
"id"=>1,
"chal"=>1,
"challenge_id"=>1,
"type"=>"static",
"flag"=>"flag{FREEPOINTS}",
"content"=>"flag{FREEPOINTS}",
"data"=>nil}
@systems.each { |system|
@@ -56,19 +55,20 @@ class CTFdGenerator
challenge_id = challenges.length + 1
challenges << {
"id"=> challenge_id,
"name"=>"",
"name"=>"Challenge ##{challenge_id}",
"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,
"state"=>"visible",
"requirements"=>"null"}
flag_id = flags.length + 1
flags << {
"id"=>flag_id,
"challenge_id"=>challenge_id,
"type"=>"static",
"flag"=>output_value,
"content"=>output_value,
"data"=>nil}
collected_hints = []
@@ -92,10 +92,11 @@ class CTFdGenerator
end
hints << {
"id"=> hint_id,
"type"=>0,
"chal"=>challenge_id,
"hint"=>collected_hint["hint_text"],
"cost"=>cost
"type"=>"standard",
"challenge_id"=>challenge_id,
"content"=>collected_hint["hint_text"],
"cost"=>cost,
"requirements"=>nil
}
}
end
@@ -104,26 +105,43 @@ class CTFdGenerator
}
output_hash = {
"alembic_version.json" => "",
"alembic_version.json" => alembic_version_json(),
"awards.json" => "",
"challenges.json" => challenges_json(challenges),
"config.json" => config_json(),
"dynamic_challenge.json" => "",
"files.json" => files_json(),
"flags.json" => flags_json(flags),
"hints.json" => hints_json(hints),
"keys.json" => keys_json(keys),
"notifications.json" => "",
"pages.json" => pages_json(),
"solves.json" => "",
"submissions.json" => "",
"tags.json" => "",
"teams.json" => teams_json(),
"teams.json" => "",
"tracking.json" => "",
"unlocks.json" => "",
"wrong_keys.json" => "",
"users.json" => users_json(),
}
output_hash
end
def alembic_version_json
alembic_version_json_hash = {
"count" => 1,
"results" => [
{
"version_num"=>"8369118943a1"
}
],
"meta"=>{}
}
alembic_version_json_hash.to_json
end
def files_json
return ''
end
@@ -137,47 +155,47 @@ class CTFdGenerator
def config_json
config_json_hash = {
"count" => 31,
"count" => 23,
"results" => [
{
"id"=>1,
"key"=>"next_update_check",
"value"=>"1529096764"
"key"=>"ctf_version",
"value"=>"2.0.2"
},
{
"id"=>2,
"key"=>"ctf_version",
"value"=>"1.2.0"
},
{
"id"=>3,
"key"=>"ctf_theme",
"value"=>"core"
},
{
"id"=>4,
"id"=>3,
"key"=>"ctf_name",
"value"=>"SecGenCTF"
},
{
"id"=>4,
"key"=>"user_mode",
"value"=>"users"
},
{
"id"=>5,
"key"=>"ctf_logo",
"value"=>nil #"fca9b07e1f3699e07870b86061815b1c/logo.svg"
"key"=>"challenge_visibility",
"value"=>"private"
},
{
"id"=>6,
"key"=>"workshop_mode",
"value"=>"0"
"key"=>"score_visibility",
"value"=>"public"
},
{
"id"=>7,
"key"=>"hide_scores",
"value"=>"0"
"key"=>"account_visibility",
"value"=>"public"
},
{
"id"=>8,
"key"=>"prevent_registration",
"value"=>"0"
"key"=>"registration_visibility",
"value"=>"public"
},
{
"id"=>9,
@@ -186,113 +204,73 @@ class CTFdGenerator
},
{
"id"=>10,
"key"=>"max_tries",
"value"=>"0"
},
{
"id"=>11,
"key"=>"end",
"value"=>nil
},
{
"id"=>12,
"id"=>11,
"key"=>"freeze",
"value"=>nil
},
{
"id"=>13,
"key"=>"view_challenges_unregistered",
"value"=>"0"
},
{
"id"=>14,
"id"=>12,
"key"=>"verify_emails",
"value"=>"0"
"value"=>nil
},
{
"id"=>15,
"id"=>13,
"key"=>"mail_server",
"value"=>nil
},
{
"id"=>16,
"id"=>14,
"key"=>"mail_port",
"value"=>nil
},
{
"id"=>17,
"id"=>15,
"key"=>"mail_tls",
"value"=>"0"
"value"=>nil
},
{
"id"=>18,
"id"=>16,
"key"=>"mail_ssl",
"value"=>"0"
"value"=>nil
},
{
"id"=>19,
"id"=>17,
"key"=>"mail_username",
"value"=>nil
},
{
"id"=>20,
"id"=>18,
"key"=>"mail_password",
"value"=>nil
},
{
"id"=>21,
"id"=>19,
"key"=>"mail_useauth",
"value"=>"0"
"value"=>nil
},
{
"id"=>20,
"key"=>"setup",
"value"=>"true"
},
{
"id"=>21,
"key"=>"paused",
"value"=>"false"
},
{
"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"
"id"=>23,
"key"=>"ctf_logo",
"value"=>nil #"fca9b07e1f3699e07870b86061815b1c/logo.svg"
}
],
"meta"=>{}
@@ -308,9 +286,9 @@ class CTFdGenerator
}.to_json
end
def keys_json(keys)
{"count"=>keys.length,
"results"=>keys,
def flags_json(flags)
{"count"=>flags.length,
"results"=>flags,
"meta"=>{}
}.to_json
end
@@ -321,19 +299,21 @@ class CTFdGenerator
"results" => [
{
"id"=>1,
"title"=>"Welcome",
"route"=>"index",
"html"=>File.read(ROOT_DIR + '/lib/templates/CTFd/index.html'),
"auth_required"=>0,
"draft"=>0,
"title"=>"Welcome"
"content"=>File.read(ROOT_DIR + '/lib/templates/CTFd/index.html'),
"draft"=>false,
"hidden"=>false,
"auth_required"=>false
},
{
"id"=>2,
"title"=>"Flag Submission",
"route"=>"submit",
"html"=>File.read(ROOT_DIR + '/lib/templates/CTFd/submit.html'),
"auth_required"=>0,
"draft"=>0,
"title"=>"Flag submission"
"content"=>File.read(ROOT_DIR + '/lib/templates/CTFd/submit.html'),
"draft"=>false,
"hidden"=>false,
"auth_required"=>true
}
],
"meta"=>{}
@@ -342,56 +322,41 @@ class CTFdGenerator
pages_json_hash.to_json
end
def teams_json
teams_json_hash = {
"count" => 2,
def users_json
# Default admin username: adminusername
# Default admin password: adminpassword
# To use an alternate password, utilize the lib/output/sha256_password.py script.
# This ensures compatibility with CTFd v2.0.2+
users_json_hash = {
"count" => 1,
"results" => [
{
"id"=>1,
"oauth_id"=>nil,
"name"=>"adminusername",
"password"=>"$bcrypt-sha256$2b,12$Fh9KaueZuSEK5YzSdTbcI.$cbJCW5wGDNBX0/C/xDvMhnv8X3vqI92",
"email"=>"admin@email.com",
"password"=>password_hash_string("adminpassword"),
"type"=>"admin",
"secret"=>nil,
"website"=>nil,
"affiliation"=>nil,
"country"=>nil,
"bracket"=>nil,
"banned"=>0,
"verified"=>1,
"admin"=>1,
"joined"=>"2018-06-22T10:46:26"
"hidden"=>true,
"banned"=>false,
"verified"=>true,
"team_id"=>nil,
"created"=>"2019-02-01T20:13:03.80374"
},
{
"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
users_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 != ""

20
lib/output/sha256_password.py Executable file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env python3
"""
Short script to encode password for use with CTFd v2.0.2+.
Requires Python3 and the following libraries:
pip install passlib passlib[bcrypt]
"""
import argparse
from passlib.hash import bcrypt_sha256
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("password", help="Password to encode to bcrypt sha256 2b variant")
args = parser.parse_args()
print("Generating password for use with CTFd v2.0.2+...\n")
hashed = bcrypt_sha256.encrypt(args.password)
print(hashed)

View File

@@ -3,8 +3,12 @@
<br>
<h1 class="text-center">Welcome to SecGenCTF</h1>
<br>
<br>
<h4 class="text-center">
Here you can <a href="submit">submit flags you discover</a>, and <a href="challenges">review the challenges, and (if available) purchase hints</a>.
You may review the <a href=\"challenges">challenges</a> and (if available) purchase hints.
</h4>
<h4 class="text-center">
Please use the <a href=\"submit">Flag Submission</a> page to submit your flags.
</h4>
<h4 class="text-center">
Good luck!

View File

@@ -12,24 +12,27 @@
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
<div class="chal-tags text-center"></div>
<p>This will submit your flag against a set of challenges (such as a VM). Navigate to <a href="challenges">Challenges</a> for hints for specific flags.</p>
</br>
<strong><p class="text-primary">Flags will be in the form of: flag{something}</p></strong>
</br>
<p class="text-danger">Note: The system has a limit of 10 flag submissions per minute. If you submit more, this screen will pause for 60 seconds and then continue. Please do not try submitting again during this time.</p>
</div>
<div class="row submit-row">
<div class="col-md-12 form-group">
<input class="form-control" type="text" name="answer" id="answer-input" placeholder="Flag" />
</div>
<div class="row submit-row">
<div class="col-md-12 form-group">
<input class="form-control" type="text" name="answer" id="answer-input" placeholder="Flag" />
</div>
<div class="col-md-12 form-group">
<select id="challenge_set" class="form-control" style="height:60px;">
</select>
</div>
<div class="col-md-12 form-group key-submit">
<button type="submit" onclick="submitflags()" id="submit-key" tabindex="5" class="btn btn-md btn-outline-secondary float-right">Submit</button>
</div>
<div class="col-md-12 form-group">
<select id="challenge_set" class="form-control" style="height:60px;">
</select>
</div>
<div class="row notification-row">
<div class="col-md-12">
<div id="result-notification" class="alert alert-dismissable text-center w-100" role="alert">
<strong id="result-message"></strong>
</div>
<div class="col-md-12 form-group key-submit">
<button type="submit" onclick="submitflags()" id="submit-key" tabindex="5" class="btn btn-md btn-outline-secondary float-right">Submit</button>
</div>
</div>
<div class="row notification-row">
<div class="col-md-12">
<div id="result-notification" class="alert alert-dismissable text-center w-100" role="alert">
<strong id="result-message"></strong>
</div>
</div>
</div>
@@ -40,89 +43,94 @@
</div>
<script>
// pause submitting, due to rate limits?
var cont = true;
// stop submitting
var abort = false;
var challenge_list;
// retrieve the list of challenges and add VMs options to selection box on load
function loadoptions() {
var chal_list = $.get(window.location.origin + "/chals", {}, function (json) {
// get the challenge list from the server, and store it for later
challenge_list = json;
// for each challenge, add the category to the selection box, if it's not been added already
$.each(json['game'], function (i, item) {
if (!$("#challenge_set option[value='" + item['category'] + "']").length) {
$('#challenge_set').append($('<option>', {
value: item['category'],
text : item['category']
}));
}
});
$('#challenge_set').append($('<option>', {
value: 'All',
text : 'All'
}));
}, 'json');
}
window.onload = loadoptions;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Spams the challenge submission with flags.
// This is necessary since with SecGen flags it's not always known which # challenge they are completing.
// Since this is all done client-side (all easily importable into CTFd), we have to pause when we hit rate limits.
// This involves some timing between asynchronous threads.
async function submitflags() {
var answer = $('#answer-input').val();
var nonce = csrf_nonce;
var url = window.location.origin + "/chal/";
$( "#result-message" ).empty();
cont = true;
abort = false;
var process_count = 0;
$.each(challenge_list['game'], async function (i, item) {
var selected = $( "#challenge_set option:selected" ).text();
if (selected == "All" || selected == item["category"]) {
process_count++;
await sleep(process_count * 500); // asynchronous spacing of queries
var chal_id = item["id"];
if (abort == true) {
return;
} else if (cont == false) {
await sleep(60000);
cont = true; // on waking this currently clobbers any subsequent pause
}
$( "#result-message" ).append('#' + chal_id + ': ');
var retval = $.post(url + chal_id, {
key: answer,
nonce: nonce
}, function (json) {
// success post
$( "#result-message" ).append(json['message'] + '<br/>');
if(json['status'] == 3) {
$( "#result-message" ).append( 'Waiting 60 seconds...<br/>' );
cont = false;
// currently doesn't retry the challenges that trigger a wait
} else if(json['status'] == 1) {
$( "#result-message" ).append( '<p class="text-success">Success! (Stopping)</p>' );
abort = true;
// pause submitting, due to rate limits?
var cont = true;
// stop submitting
var abort = false;
var challenge_list;
// retrieve the list of challenges and add VMs options to selection box on load
function loadoptions() {
var chal_list = $.get(window.location.origin + "/api/v1/challenges", {}, function (json) {
// get the challenge list from the server, and store it for later
challenge_list = json;
// for each challenge, add the category to the selection box, if it's not been added already
$.each(json.data, function (i, item) {
if (!$("#challenge_set option[value='" + item.category + "']").length) {
$('#challenge_set').append($('<option>', {
value: item['category'],
text : item['category']
}));
}
}, 'json');
retval.fail(function() {
alert( "error" );
})
}
});
}
});
$('#challenge_set').append($('<option>', {
value: 'All',
text : 'All'
}));
}, 'json');
}
window.onload = loadoptions;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Spams the challenge submission with flags.
// This is necessary since with SecGen flags it's not always known which # challenge they are completing.
// Since this is all done client-side (all easily importable into CTFd), we have to pause when we hit rate limits.
// This involves some timing between asynchronous threads.
async function submitflags() {
var answer = $('#answer-input').val();
var nonce = csrf_nonce;
var url = window.location.origin + "/api/v1/challenges/attempt";
$( "#result-message" ).empty();
cont = true;
abort = false;
var process_count = 0;
var delayed = [];
$.each(challenge_list.data, async function (i, item) {
var selected = $( "#challenge_set option:selected" ).text();
if (selected == "All" || selected == item["category"]) {
process_count++;
await sleep(process_count * 1000); // asynchronous spacing of queries
var chal_id = item["id"];
if (abort == true) {
return;
} else if (cont == false) {
await sleep(60000);
cont = true; // on waking this currently clobbers any subsequent pause
}
var result_message = $( '#result-message' );
result_message.append( 'Challenge #' + chal_id + ' : ' );
var retval = $.post(url, {
challenge_id: chal_id,
submission: answer,
nonce: nonce
}, function (json) {
var result = json.data;
if (result.status === "incorrect") { //Incorrect Key
result_message.append( '<p class="text-danger">' + result.message + '</p></br>' );
} else if (result.status === "correct") { //Challenge Solved
result_message.append( '<p class="text-success">' + result.message + '</p></br>' );
abort = true;
} else if (result.status === "already_solved") { //Challenge Already Solved
result_message.append( '<p class="text-warning">' + result.message + '</p></br>' );
}
}, 'json');
retval.fail(function(xhr, status, text) {
var result = xhr.responseJSON.data;
if (xhr.status === 429) { //Keys per minute too high
result_message.append( '<p class="text-danger">' + result.message + ' Pausing for 60 seconds. Do not hit Submit again.</p></br>' );
delayed.push(chal_id);
cont = false;
} else if (xhr.status === 403) {
if (result.status === "paused") { //CTF is paused
result_message.append( '<p class="text-danger">' + result.message + ' (Aborting...)</p></br>' );
abort = true;
} else if (result.status === "authentication_required") { //User is logged out
window.location = script_root + "/login?next=" + script_root + window.location.pathname + window.location.hash;
return
}
}
});
}
});
}
</script>