This page looks best with JavaScript enabled

EZCTF, a full stack webapp..

 ·  ☕ 8 min read

Intro

Ever sit down to learn something and at first the examples are great, but then when it comes to getting advanced – you’re lost wondering what happened inbetween? When learning I always found it easy to get started, but hard to get anywhere meaningful as all the examples felt like this picture

Owl

It was always hard to find comprehensive code examples that covered a wide range of topics, so I figured I would share this project in hopes someone takes away some newfound information or motivation.


Pre-Reqs

All the pieces at work in the project come from the below technologies and documentation.

In short what I’ve done here is written my own web app, conformed it to the specs demonstrated in the Flask blog tutorial, then added my own supporting structures around it.

Whenever there was a need for a specific feature, I would check the following docs to make sure I wasn’t reinventing the wheel. Otherwise it was poignant to roll my own code for the feature.

  1. Theory - Software Development Life Cycle
  2. Database - SQLAlchemy
  3. Database - PyMySql
  4. Back End - Flask Tutorial
  5. Front End - Bootstrap4
  6. Front End - Jinja2/Flask
  7. Tests - PyTests/Flask
  8. Tests - UnitTest/Selenium
  9. Automation - Ansible
  10. AWS Deployment - Terraform

Sometimes you may find features in libraries that kind of meet your use case, if you’re lucky you can extend or build upon their code to make it work for you.


EZCTF

Coming back from DEFCON I was beyond amped to write some code. It was about time to finally complete a full stack web project. Having participated in the OPENCTF at DC26, I had a great idea. Why not make a little hacking game to play with some colleagues?

The mission of this writeup: by showing some different aspects of creating a webapp that isnt a standard example, I hope to illustrate different ways basic concepts come together to achieve more nuanced features. Getting started was never a problem, but we’ve all spent alot of time staring at the blinking cursor wondering what next.

So what is a CTF?

Capture the Flag (CTF) is a kind of computer security competition. CTF contests are usually designed to serve as an educational exercise to give participants experience in securing a machine, as well as conducting and reacting to the sort of attacks found in the real world.

Having a clear cut goal from the beginning is key to building clean code. We want to set our scope and stick to it.

In our case, all we want is to make a web application that will receive flags and track a user’s score on the scoreboard.

For added robustness we will add some admin features and tools to make our lives easier. Without further ado, lets dive in.

Incase you’d like to just read the code, here you go


Project Directory Structure

Theres obviously many more subdirectories, but for explanation we only need to concern ourselves with these four.

ezctf
├── ezctf-alpha
├── ezctf-ansible
├── ezctf-terraform
├── linted_ctf
39 directories, 71 files
---
  • alpha - The first version of the app where all functionality was prototyped
  • linted - The cleaned up version of the app where all functionality is modular and code coverage is provided by Tests
  • Ansible - The Configuration Management tools used to configure Vagrant and Linux to run our application
  • Terraform - Infrastructure As Code providing us with both infrastructure deployment and provisioning

Back End

Due to the ease of use and speed of prototyping you can achieve, Python was the natural choice for the back end. Here we use Flask as it has minimal overhead allowing us to pick and choose our pieces.

This is an overly simplistic explanation of the back end, if you want to learn how Flask works read the linked above tut.

Originally in the alpha everything was in one app.py file, thats obviously not great, so we split it out. Using Blueprints from the Flask tutorial we wind up with 3 files running our app.

app/
├── auth.py
├── cruds.py
├── extensions.py
├── __init__.py

The __init__.py provides an entry point for Gunicorn, which is what will handle the HTTP requests so that Flask doesnt have to, Gunicorn is typically what goes infront of Flask or Django in production environments.

auth.py is fairly self explanatory, it handles authentication for our users.

cruds.py is the bulk of our logic and the most important part here. When planning functionality of our app, CRUDS provides a great starting point.

  • Create
  • Read
  • Update
  • Delete

This helps us layout initial functionality for our app, from there we can begin to add CTF specific features – such as when Updating a Challenge to show add a completion, update the users score who solved it as well.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
if request.method == 'POST' and form.validate():

    submitted_flag = request.form['flag']

    if submitted_flag == flag and int(solved[0]['f']) != 1:

        #Add the flag to the users so we can keep track of their challenges
        update_flags = cur.execute('UPDATE users set flags = JSON_SET(flags, %s, 1) WHERE username = %s', (cid,[session['username']]))
        mysql.connection.commit()

        #Update the users score to show they solved the challenge
        get_score = cur.execute('SELECT score FROM users WHERE username = %s', [session['username']])
        user_score = cur.fetchone()
        new_score = (int(user_score['score']) + int(article['ch_score']))
        updated_score = cur.execute('UPDATE users set score = %s WHERE username = %s', (new_score,[session['username']]))
        mysql.connection.commit()

        #Update the challenges number of solves
        get_solves = cur.execute('SELECT ch_solves from challenges WHERE ch_id = %s', ([id]))
        solves =  cur.fetchone()
        update_solves = int(solves['ch_solves']) + 1 
        updated_solves = cur.execute('UPDATE challenges set ch_solves = %s where ch_id =%s', (update_solves, [id]))
        mysql.connection.commit()

        flash('Good job! Your score has been updated','success')
        cur.close()
        return redirect(url_for('cruds.dashboard'))

    else:
        cur.close()
        flash('Incorrect answer', 'danger')

So theres some of our backend functioanlity to handle scoring, we can elaborate more on the SQL in the database portion. When users solve the challenges all of this is put into the Scoreboard page, a fixture CTFs everywhere.

Scoreboard

Front End

Front end design is not a favorite of mine, so we’ll let Bootstrap4 and Jinja2 do all the work for us.

The goal here isnt to teach you to make a front end or use Jinaj2 like a pro, its to hopefully give you something to draw upon when you’re trying to think of the code you need to put in your application.

In short Jinja2 is a templating engine, so we can give it a placeholders and Flask will inject the content and render it as HTML on the front end.

Next up, we’ve got the Challenges page laid out in a jeopardy style. It was a mild pain at first to make the grid layout but a little help from the Modulus operator alongside the Jinja2 docs and we’ve got the code to render it in our desired layout.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{% for challenge in challenges %}

  {% if (loop.index) % 3 == 1 %}
    <div class="row">
  {% endif %}  
 
      <div class="col-sm">
       <div class="card">

          <div class="card-header"></div>
          
          <div class="card-body"></div>
          

       </div>
      </div>

  {% if loop.index % 3 == 0 or loop.last  %}

    {% if loop.last and loop.index % 3 == 1 %}
            <div class="col-sm"></div>
            <div class="col-sm"></div>
    {% endif %}

    {% if loop.last and loop.index % 3 == 2 %}
            <div class="col-sm"></div>
    {% endif %}

   </div id="row-close"><!-- /row --> 
        
  {% endif %} 
{% endfor %}

Which turns out to look like this

Challenge

Hitting the floor of our front end, the last thing to look at is the Challenges page. We use Jinaj2 and Flask to render parts of page based on a users context such as; Is_Admin or Is_Logged_In.

Achieved easily by using some simple conditionals and passing the Session object into the template.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{% if session.logged_in %}
  <h2>Submit Flag</h2>
    <div class="form-group">
      {{ render_field(form.flag, class_="form-control") }}
    </div>
    <p><input class="btn btn-success" type="submit" value="Submit"></p>
  </form>
    {% if article.ch_id == 1 %}
     <!-- Youre very close but Im a little more tricky than that. -->
    {% endif %}

   {% if session.admin %}
     <h2> Admin panel </h2>
   {% endif %}

   {% else %}

     <p> {{message}}</p>

{% endif %}

Logged out view


chal_logedout.png

Logged in view


challenge1.png

Tests

Test coverage – a measure used to describe the degree to which the source code of a program is executed when a particular test suite runs.

We have two kinds of test coverage at play here.

  1. PyTest - Used before committing code, based off the example laid out in the Flask tutorial.

Pytest

  1. UnitTest/Selenium - Used post deployment to check renders and verify page content.

Pytest

Oh would you look at that, a failure. We changed the About page to be Rules so we need to change our tests to match, hence the second one succeeding.

Drake on unit tests

For those familiar with test driven development – the above picture probably made you twitch a little, but after debugging failed tests you begin to understand how the creator mustve felt.

You can run Selenium headless, which works great if we want to shove it into a CI/CD pipeline in order to validate our deployments.

Otherwise running it with the Chrome Driver we can see the tests iterating over our application.

db results

Each suite has its own structure and fixtures you can use to simplify to your testing, just go with what fits your use caser best..

In general it can be difficult to wrap your head around unit tests but by working them into your code you will be able to prevent against Undefined behavior, which in alot of cases leads to security issues. My unit tests don’t hit 100% code coverage but in an ideal world that is what we would want to strive for.

To be continued

This post was dragging on quite a bit so check out part 2 for the remaining topics

  • Databases
  • Ansible
  • Terraform

Coming soon!

Share on

Anthony Laiuppa
WRITTEN BY
Anthony Laiuppa
DevSecOps Engineer