FwordCTF2k21 Automation scripts

FwordCTF2k21

Introduction

Last August our team organized FwordCTF2k21 and it was a successful event and we had a lot of positive feedbacks. In this article, I am going to talk about all the automation scripts we used to ensure that everything went accordingly from health checks to interacting with the participants.

Note: this article is for documentation purposes and all the links will be below.

Summury

1- Tasks Health check
2- Discord Bot
3- Certificates script
4- Conclusion

Tasks-health-check

Last year we had a major problem, we didn’t have any way to get notified if a task or the platform is down only from participants xD. So in his year edition, we used newRelic as a way to monitor all the servers and it was very helpful and it saved us many times. But we wanted something that notifies us in discord since all the admins were mostly active there, so we wrote a simple script that when a task is down it will automatically send a message pinging all the admin with information about which task had problems the script takes a JSON file with a certain structure for every task and tests the task status depending on the task’s category.

the JSON file:
enter image description here

The alert message will look like this:

enter image description here
the script starts by checking the task’s type from the data in the JSON file

def check(task):
	if task["type"] == "web":
		link = task['server']+":"+task["port"]
		try:
			r = requests.get(link)
			if r.status_code == 200:
				if task["out"] in r.text:
					return 1
				else:
					return 0
			else:
				return 0
		except:
			return 0
	elif task["type"] == "service":
		try:
			r = remote(task["server"],task["port"])
			r.send(task["in"])
			data = r.recv()
			if data in task["out"]:
				return 1
			return 0
		except:
			return 0
	pass

Then it will use either pwntools or python requests to test the task.

For the web tasks, it just sends a GET request and if the status code is 200 it checks the output. Now for the services tasks, it uses pwntools library and connects to the server and if the response has the output in the JSON file then everything is ok. I wanted to include the health check as a functionality in the Discord bot, but unfortunately, I didn’t have much time.

Discord Bot


enter image description here

Now for the discord bot, we wanted it to do two main tasks which are giving the welcome flag to the participants and announcing all the first blood. the first task was pretty much straight forward we just used the discord bot library. the bot has basics commands like help and ping to test the latency and of course the flag command to give the flags for the participants.

@bot.command()
async def flag(ctx):
    await ctx.send("Noooo not here the good stuff is a secret 🤫 \nhit me up in my dms(message the bot privately)")


enter image description here

It listens for a private message and if it finds the word flag it will send it.

@bot.listen()
async def on_message(message):
    if "flag" == message.content.lower():
        try:
            if message.channel.id == message.author.dm_channel.id:
                #await message.channel.send('**THE CTF HASN\'t STARTED YET**')
                await message.channel.send('**HAVE FUN**\nFwordCTF{Welcome_To_FwordCTF_2021}')
                await bot.process_commands(message)
        except:
            pass


enter image description here

For the second task, we found a lot of solutions but the easiest one, at least for us, was to use selenium to scrape the data needed for the first blood. The bot at first starts with collecting all the task names when lunched and storing all the data in an array.

@bot.event
async def on_ready():
    await bot.change_presence(activity=discord.Activity(url="https://fword.tech/"))
    print('The Bot is Ready')
    options = Options()
    options.add_argument('--headless')
    options.add_argument('--disable-gpu')

    usernameStr = '###CTFd_CREDS###'
    passwordStr = '###CTFd_CREDS###'

    browser = webdriver.Chrome(ChromeDriverManager().install(), options=options )

    print('loading')
    browser.get(('https://ctf.fword.tech/login'))
    print('loaded')
    time.sleep(20)
    username = browser.find_element_by_id('name')
    username.send_keys(usernameStr)
    password = browser.find_element_by_id('password')
    password.send_keys(passwordStr)

    submitBtn = browser.find_element_by_class_name('btn')
    submitBtn.click()
    browser.get(f'https://ctf.fword.tech/api/v1/challenges')
    html = browser.page_source
    time.sleep(2)
    html = html[html.index('{'):html.rindex('}')+1]
    y = json.loads(html)
    for i in range(len(y['data'])):
        ch[y['data'][i]['id']] = {'name':y['data'][i]['name'],'solved': False}
    print(ch)
    await firstBlood.start()

And then every 3 minutes a bot tasks loop checks for first blood if there is one marks the task as solved and sends a message to the discord server containing the team that had the first blood and the task name:

@tasks.loop(seconds=180)
async def firstBlood():

    allSolved = True
    keys = dict.keys(ch)
    for i in keys:
        if not(ch[i]['solved']):
            allSolved = False

    if(allSolved):
        return
    
    channel = bot.get_channel(int('###CHANNEL_ID###'))

    options = Options()
    options.add_argument('--headless')
    options.add_argument('--disable-gpu')

    usernameStr = '###CTFd_CREDS###'
    passwordStr = '###CTFd_CREDS###'

    browser = webdriver.Chrome(ChromeDriverManager().install(), options=options )

    print('loading')
    browser.get(('https://ctf.fword.tech/login'))
    print('loaded')

    username = browser.find_element_by_id('name')
    username.send_keys(usernameStr)
    password = browser.find_element_by_id('password')
    password.send_keys(passwordStr)

    submitBtn = browser.find_element_by_class_name('btn')
    submitBtn.click()


    for i in keys:
        
        if(ch[i]['solved']):
            continue

        browser.get(f'https://ctf.fword.tech/api/v1/challenges/{i}/solves')
        html = browser.page_source
        time.sleep(2)
        html = html[html.index('{'):html.rindex('}')+1]
        y = json.loads(html)
            
        try: y['data']
        except:
            print('key error')
            continue

        if(y['data']==[]):
            print(f'no data for {ch[i]}')
            continue

        ch[i]['solved'] = True
        # print(f'`First blood for challenge: {ch[i]["name"]} goes to {y["data"][0]["name"]}`')
        print("sending")
        await channel.send(f'```css\n🩸 First blood for .{ch[i]["name"]} goes to [{y["data"][0]["name"]}]```')
        print(ch)
    browser.close()
    print('Completed!')


enter image description here

btw credit to the CSICTF admin for his amazing article it helped me a lot(link below)

Certificates script

In this year edition, we had more than 2000 participants and more than 1K teams that made it to the scoreboard so no way we were going to do the certificates manually and send them for the teams so we opted to write a script that generate all the certificates and send them via email to the teams.

So to generate the certificates we started with a template done by our talented designer Aptx and we used pillow library in python to put the team name, points, and ranking and the result was a pdf containing the certificate that will be attached to the email.

im = Image.open(r'Certificate.png')
d = ImageDraw.Draw(im)
text_color = (150, 192, 14)
font = ImageFont.truetype("NeusaNextPro-Light.ttf", 250)
w, h = d.textsize(i['name'], font=font)
location = ((2700-w)/2, 1000)
d.text(location, i['name'], fill = text_color, font = font)
#write score
text_color = (255, 255, 255)
font = ImageFont.truetype("NeusaNextPro-Regular.ttf", 100)
w, h = d.textsize(str(i['score']), font=font)
location = ((4250-w)/2, 1650)
d.text(location, str(i['score']), fill = text_color, font = font)
#write positions
text_color = (255, 255, 255)
font = ImageFont.truetype("NeusaNextPro-Regular.ttf", 100)
w, h = d.textsize(str(i['pos'])+"#", font=font)
location = ((1100-w)/2, 1650)
d.text(location, str(i['pos'])+"#", fill = text_color, font = font)
im.save("certificate_" + str(i['pos']) +".pdf")

Template
enter image description here

Result
enter image description here

Now for the emails, we opted for Sendgrid because we were most familiar with it and we worked with it before in the FwordCTF first edition

for those who don’t know it Sendgrid is a platform used mainly for email marketing you can customize the emails and use the API key to use it in an automation script.

here are some stats about the emails we sent during the period of FwordCTF
enter image description here

For the emails template, we used one for the invitation before the CTF and one for sending the certificates.
enter image description here
enter image description here

sg = sendgrid.SendGridAPIClient(api_key="###SENDGRID_API_KEY###")
        from_email = Email("contact@fword.tech")
        x = id_list.index(i['members'][0]['id'])
        to_email = To(mail_list[x])
        mail = Mail(from_email, to_email)
        mail.dynamic_template_data = {'team' : i['name']}
        mail.template_id = "###SENDGRID_TEMPLATE_ID###"
        #with open("certificate_" + i +".pdf", 'rb') as sigf:
        #    sig = sigf.read()
        sig = open("certificate_" + str(i['pos']) +".pdf", "rb").read()
        encoded = base64.b64encode(sig).decode()
        attachment = Attachment()
        attachment.file_content = FileContent(encoded)
        attachment.file_type = FileType('pdf')
        attachment.file_name = FileName("certificate_" + str(i['pos']) +".pdf")
        attachment.disposition = Disposition('attachment')
        attachment.content_id = ContentId('Example Content ID')
        mail.attachment = attachment
        response = sg.client.mail.send.post(request_body=mail.get())

The code attache the certificate and fill the placeholders thens end it using sendfrid api the team mail that is stored in an excel sheet

Conclusion

we know these are simple scripts but they did a big role in managing the CTF and made it a lot easier than doing all these tasks manually, these scripts had a lot of issues like for the bot if anything happens the data will be lost and it will start the first blood from the begging and also the health check can have a lot of false alerts s for net year edition we set the goal to combine all these tasks in one bot that it will manage everything from the beginning of the CTF with no human interaction it will be more optimized and it will help us focus more interacting with the participants and doing more challenges.

In the next article, we will break down the main infrastructure and all its components and we will talk in detail about all the challenges and problems we faced and the solution we went with.

CSICTF bot: https://medium.com/csictf/kuwu-a-discord-utility-bot-for-ctf-s-5727ea5a6019
All the source code: https://github.com/H4MA-A/FwordCTF

Mohamed Arfaoui
Mohamed Arfaoui
Information Security Engineer/Developer

A cybersecurity enthusiast specialist in reverse engineering and malware analysis.

Related