Files
CTFd/populate.py
Kevin Chung adc70fb320 3.0.0a1 (#1523)
Alpha release of CTFd v3. 

# 3.0.0a1 / 2020-07-01

**General**

- CTFd is now Python 3 only
- Render markdown with the CommonMark spec provided by `cmarkgfm`
- Render markdown stripped of any malicious JavaScript or HTML.
  - This is a significant change from previous versions of CTFd where any HTML content from an admin was considered safe.
- Inject `Config`, `User`, `Team`, `Session`, and `Plugin` globals into Jinja
- User sessions no longer store any user-specific attributes.
  - Sessions only store the user's ID, CSRF nonce, and an hmac of the user's password
  - This allows for session invalidation on password changes
- The user facing side of CTFd now has user and team searching
- GeoIP support now available for converting IP addresses to guessed countries

**Admin Panel**

- Use EasyMDE as an improved description/text editor for Markdown enabled fields.
- Media Library button now integrated into EasyMDE enabled fields
- VueJS now used as the underlying implementation for the Media Library
- Fix setting theme color in Admin Panel
- Green outline border has been removed from the Admin Panel

**API**

- Significant overhauls in API documentation provided by Swagger UI and Swagger json
- Make almost all API endpoints provide filtering and searching capabilities
- Change `GET /api/v1/config/<config_key>` to return structured data according to ConfigSchema

**Themes**

- Themes now have access to the `Configs` global which provides wrapped access to `get_config`.
  - For example, `{{ Configs.ctf_name }}` instead of `get_ctf_name()` or `get_config('ctf_name')`
- Themes must now specify a `challenge.html` which control how a challenge should look.
- The main library for charts has been changed from Plotly to Apache ECharts.
- Forms have been moved into wtforms for easier form rendering inside of Jinja.
  - From Jinja you can access forms via the Forms global i.e. `{{ Forms }}`
  - This allows theme developers to more easily re-use a form without having to copy-paste HTML.
- Themes can now provide a theme settings JSON blob which can be injected into the theme with `{{ Configs.theme_settings }}`
- Core theme now includes the challenge ID in location hash identifiers to always refer the right challenge despite duplicate names

**Plugins**

- Challenge plugins have changed in structure to better allow integration with themes and prevent obtrusive Javascript/XSS.
  - Challenge rendering now uses `challenge.html` from the provided theme.
  - Accessing the challenge view content is now provided by `/api/v1/challenges/<challenge_id>` in the `view` section. This allows for HTML to be properly sanitized and rendered by the server allowing CTFd to remove client side Jinja rendering.
  - `challenge.html` now specifies what's required and what's rendered by the theme. This allows the challenge plugin to avoid having to deal with aspects of the challenge besides the description and input.
  - A more complete migration guide will be provided when CTFd v3 leaves beta
- Display current attempt count in challenge view when max attempts is enabled
- `get_standings()`, `get_team_stanadings()`, `get_user_standings()` now has a fields keyword argument that allows for specificying additional fields that SQLAlchemy should return when building the response set.
  - Useful for gathering additional data when building scoreboard pages
- Flags can now control the message that is shown to the user by raising `FlagException`
- Fix `override_template()` functionality

**Deployment**

- Enable SQLAlchemy's `pool_pre_ping` by default to reduce the likelihood of database connection issues
- Mailgun email settings are now deprecated. Admins should move to SMTP email settings instead.
- Postgres is now considered a second class citizen in CTFd. It is tested against but not a main database backend. If you use Postgres, you are entirely on your own with regards to supporting CTFd.
- Docker image now uses Debian instead of Alpine. See https://github.com/CTFd/CTFd/issues/1215 for rationale.
- `docker-compose.yml` now uses a non-root user to connect to MySQL/MariaDB
- `config.py` should no longer be editting for configuration, instead edit `config.ini` or the environment variables in `docker-compose.yml`
2020-07-01 12:06:05 -04:00

356 lines
11 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
import datetime
import hashlib
import random
import argparse
from CTFd import create_app
from CTFd.cache import clear_config, clear_standings, clear_pages
from CTFd.models import (
Users,
Teams,
Challenges,
Flags,
Awards,
ChallengeFiles,
Fails,
Solves,
Tracking,
)
from faker import Faker
fake = Faker()
parser = argparse.ArgumentParser()
parser.add_argument("--mode", help="Set user mode", default="teams")
parser.add_argument("--users", help="Amount of users to generate", default=50, type=int)
parser.add_argument("--teams", help="Amount of teams to generate", default=10, type=int)
parser.add_argument(
"--challenges", help="Amount of challenges to generate", default=20, type=int
)
parser.add_argument(
"--awards", help="Amount of awards to generate", default=5, type=int
)
args = parser.parse_args()
app = create_app()
mode = args.mode
USER_AMOUNT = args.users
TEAM_AMOUNT = args.teams if args.mode == "teams" else 0
CHAL_AMOUNT = args.challenges
AWARDS_AMOUNT = args.awards
categories = [
"Exploitation",
"Reversing",
"Web",
"Forensics",
"Scripting",
"Cryptography",
"Networking",
]
companies = ["Corp", "Inc.", "Squad", "Team"]
icons = [
None,
"shield",
"bug",
"crown",
"crosshairs",
"ban",
"lightning",
"code",
"cowboy",
"angry",
]
def gen_sentence():
return fake.text()
def gen_name():
return fake.first_name()
def gen_team_name():
return fake.word().capitalize() + str(random.randint(1, 1000))
def gen_email():
return fake.email()
def gen_category():
return random.choice(categories)
def gen_affiliation():
return (fake.word() + " " + random.choice(companies)).title()
def gen_value():
return random.choice(range(100, 500, 50))
def gen_word():
return fake.word()
def gen_icon():
return random.choice(icons)
def gen_file():
return fake.file_name()
def gen_ip():
return fake.ipv4()
def random_date(start, end):
return start + datetime.timedelta(
seconds=random.randint(0, int((end - start).total_seconds()))
)
def random_chance():
return random.random() > 0.5
if __name__ == "__main__":
with app.app_context():
db = app.db
# Generating Challenges
print("GENERATING CHALLENGES")
for x in range(CHAL_AMOUNT):
word = gen_word()
chal = Challenges(
name=word,
description=gen_sentence(),
value=gen_value(),
category=gen_category(),
)
db.session.add(chal)
db.session.commit()
f = Flags(challenge_id=x + 1, content=word, type="static")
db.session.add(f)
db.session.commit()
# Generating Files
print("GENERATING FILES")
AMT_CHALS_WITH_FILES = int(CHAL_AMOUNT * (3.0 / 4.0))
for x in range(AMT_CHALS_WITH_FILES):
chal = random.randint(1, CHAL_AMOUNT)
filename = gen_file()
md5hash = hashlib.md5(filename.encode("utf-8")).hexdigest()
chal_file = ChallengeFiles(
challenge_id=chal, location=md5hash + "/" + filename
)
db.session.add(chal_file)
db.session.commit()
# Generating Teams
print("GENERATING TEAMS")
used = []
used_oauth_ids = []
count = 0
while count < TEAM_AMOUNT:
name = gen_team_name()
if name not in used:
used.append(name)
team = Teams(name=name, password="password")
if random_chance():
team.affiliation = gen_affiliation()
if random_chance():
oauth_id = random.randint(1, 1000)
while oauth_id in used_oauth_ids:
oauth_id = random.randint(1, 1000)
used_oauth_ids.append(oauth_id)
team.oauth_id = oauth_id
db.session.add(team)
count += 1
db.session.commit()
# Generating Users
print("GENERATING USERS")
used = []
used_oauth_ids = []
count = 0
while count < USER_AMOUNT:
name = gen_name()
if name not in used:
used.append(name)
try:
user = Users(name=name, email=gen_email(), password="password")
user.verified = True
if random_chance():
user.affiliation = gen_affiliation()
if random_chance():
oauth_id = random.randint(1, 1000)
while oauth_id in used_oauth_ids:
oauth_id = random.randint(1, 1000)
used_oauth_ids.append(oauth_id)
user.oauth_id = oauth_id
if mode == "teams":
user.team_id = random.randint(1, TEAM_AMOUNT)
db.session.add(user)
db.session.flush()
track = Tracking(ip=gen_ip(), user_id=user.id)
db.session.add(track)
db.session.flush()
count += 1
except Exception:
pass
db.session.commit()
if mode == "teams":
# Assign Team Captains
print("GENERATING TEAM CAPTAINS")
teams = Teams.query.all()
for team in teams:
captain = (
Users.query.filter_by(team_id=team.id)
.order_by(Users.id)
.limit(1)
.first()
)
if captain:
team.captain_id = captain.id
db.session.commit()
# Generating Solves
print("GENERATING SOLVES")
if mode == "users":
for x in range(USER_AMOUNT):
used = []
base_time = datetime.datetime.utcnow() + datetime.timedelta(
minutes=-10000
)
for y in range(random.randint(1, CHAL_AMOUNT)):
chalid = random.randint(1, CHAL_AMOUNT)
if chalid not in used:
used.append(chalid)
user = Users.query.filter_by(id=x + 1).first()
solve = Solves(
user_id=user.id,
team_id=user.team_id,
challenge_id=chalid,
ip="127.0.0.1",
provided=gen_word(),
)
new_base = random_date(
base_time,
base_time
+ datetime.timedelta(minutes=random.randint(30, 60)),
)
solve.date = new_base
base_time = new_base
db.session.add(solve)
db.session.commit()
elif mode == "teams":
for x in range(1, TEAM_AMOUNT):
used_teams = []
used_users = []
base_time = datetime.datetime.utcnow() + datetime.timedelta(
minutes=-10000
)
team = Teams.query.filter_by(id=x).first()
members_ids = [member.id for member in team.members]
for y in range(random.randint(1, CHAL_AMOUNT)):
chalid = random.randint(1, CHAL_AMOUNT)
user_id = random.choice(members_ids)
if (chalid, team.id) not in used_teams:
if (chalid, user_id) not in used_users:
solve = Solves(
user_id=user_id,
team_id=team.id,
challenge_id=chalid,
ip="127.0.0.1",
provided=gen_word(),
)
new_base = random_date(
base_time,
base_time
+ datetime.timedelta(minutes=random.randint(30, 60)),
)
solve.date = new_base
base_time = new_base
db.session.add(solve)
db.session.commit()
used_teams.append((chalid, team.id))
used_users.append((chalid, user_id))
db.session.commit()
# Generating Awards
print("GENERATING AWARDS")
for x in range(USER_AMOUNT):
base_time = datetime.datetime.utcnow() + datetime.timedelta(minutes=-10000)
for _ in range(random.randint(0, AWARDS_AMOUNT)):
user = Users.query.filter_by(id=x + 1).first()
award = Awards(
user_id=user.id,
team_id=user.team_id,
name=gen_word(),
value=random.randint(-10, 10),
icon=gen_icon(),
)
new_base = random_date(
base_time,
base_time + datetime.timedelta(minutes=random.randint(30, 60)),
)
award.date = new_base
base_time = new_base
db.session.add(award)
db.session.commit()
# Generating Wrong Flags
print("GENERATING WRONG FLAGS")
for x in range(USER_AMOUNT):
used = []
base_time = datetime.datetime.utcnow() + datetime.timedelta(minutes=-10000)
for y in range(random.randint(1, CHAL_AMOUNT * 20)):
chalid = random.randint(1, CHAL_AMOUNT)
if chalid not in used:
used.append(chalid)
user = Users.query.filter_by(id=x + 1).first()
wrong = Fails(
user_id=user.id,
team_id=user.team_id,
challenge_id=chalid,
ip="127.0.0.1",
provided=gen_word(),
)
new_base = random_date(
base_time,
base_time + datetime.timedelta(minutes=random.randint(30, 60)),
)
wrong.date = new_base
base_time = new_base
db.session.add(wrong)
db.session.commit()
db.session.commit()
db.session.close()
clear_config()
clear_standings()
clear_pages()