Reply Cyber Security Challenge 2020 writeups

Writeup ott 12, 2020

Last weekend we played the Reply Cyber Security Challenge 2020 and we solved the four challenges you find in this article.
You can find files and exploits on our repository, at the url: https://github.com/r00tstici/writeups/tree/master/Reply-Cyber-Security-Challenge-2020

Wells-read

Category: Coding

Points: 200

Solved by: 0xThorn

Problem

Description: R-Boy finds the time machine, which Zer0 used to travel to another era. Nearby, lies a copy of HG Wells’ The Time Machine. Could this book from the past help R-Boy travel in time? Unfortunately, R-Boy can’t read or understand it, because the space-time continuum has changed some of the words. Help R-Boy to start his time travels!

Writeup

Analysis

We are provided with two files.

At first glance, the first file (The time machine...) looks like a book, but looking closely at the text you can see that some words have wrong characters.
Instead the second file (words.txt) contains a long list of words, practically a dictionary.

Looking closely at the wrong characters in the first file, you notice that some are symbols, such as curly brackets.
From there comes the intuition: the flag was hidden in the text by substituting characters in the original words!

The script

TL;DR you can find the solution script here https://github.com/r00tstici/writeups/blob/master/Reply-Cyber-Security-Challenge-2020/coding-100/exploit/solve.py

  1. Imports the book by removing the new lines, double dashes and double spaces
with open('The Time Machine by H. G. Wells.txt','r') as file:   
    text = file.read().replace("\n", " ").replace('--', ' ').replace('  ', ' ')
  1. Finds all the strings enclosed in two curly brackets (for the format flag)
matches = re.findall(r'\{[^\{\}]*\}', text)
  1. For each of the strings it is necessary to find the defects in the single words
for result in matches:
  differences = ''
  for word in result.split():
    differences += find_differences(word)
  print(differences)
  1. We create the find_differences function to find the defects in a word. First, punctuation characters are removed at the beginning and at the end
beginning_punctation = ['"', "'"]
end_punctation = [',', '.', '!', '?', '"', "'", ';', ":"]

while len(word) > 1 and word[-1] in end_punctation:
  word = word[:-1]

while len(word) > 1 and word[0] in beginning_punctation:
  word = word[1:]
  1. Then it checks that the word is not already perfectly contained in the dictionary. Checks are made with lowercase words to avoid false differences due to capitalization.
if word.lower() in [x.lower() for x in dictionary]:
  return ''
  1. The comparison is made with each word in the dictionary, only if the length of the words matches. It does a character-by-character check and saves the differences. If there is only one different character, the search ends.
for d in dictionary:
  if len(d) == len(word):
    if word[0].islower():
      d = first_lower(d)
    else:
      d = first_upper(d)

    different_characters = ''
    for _ in range(len(word)):
      if d[_] != word[_]:
        different_characters += word[_]

    if len(different_characters) == 1:
      return different_characters

return ''
  1. Run the script and get multiple strings. Only one respects the flag format: {FLG:1_kn0w_3v3ryth1ng_4b0ut_t1m3_tr4v3ls}

LimboZone -?-> LimboZ0ne

Category: coding

Points: 200

Solved by: drw0if, 0xThorn

Problem

At first, R-Boy discovers the ‘limbo zone’ where, caught in a trap, he meets Virgilia, a guide and protector of the temporal dimension. Virgilia has probably been trapped by Zer0, but R-Boy can release her by decrypting the code.

Writeup

We are provided with a 7z archive, decompressing it we got 4 files:

  • level_0.png
  • lev3l_0.png
  • ForceLevel.py
  • level_1.7z

The .pngs are two seems equal pictures, the new archive is password protected and the python script contains:

# ForceLevel.py
def tryOpen(password):
    # TODO
    pass


def main():
    for x in range(0, 10000):
        for y in range(0, 10000):
            for r1 in range (0, 256):
                for g1 in range (0, 256):
                    for b1 in range (0, 256):
                        for r2 in range (0, 256):
                            for g2 in range (0, 256):
                                for b2 in range (0, 256):
                                    xy = str(x) + str(y)
                                    rgb1 = '{:0{}X}'.format(r1, 2) + '{:0{}X}'.format(g1, 2) + '{:0{}X}'.format(b1, 2)
                                    rgb2 = '{:0{}X}'.format(r2, 2) + '{:0{}X}'.format(g2, 2) + '{:0{}X}'.format(b2, 2)
                                    tryOpen(xy + rgb1 + rgb2)


if __name__ == "__main__":
    main()

So it is clear enough that in some weird way we must recover the password starting from the two pictures. We wrote a few python lines to loop the two pictures and find the only position whose pixels are different in the two images, we then applied the same composition from the provided script and we got a string, probably the archive password. We firstly tried to open the archive but we got wrong password... We applied the script again inverting che images so that first comes level_0.png and then lev3l_0.png. We got the right ones so we passed the level.

191186E2DBDB1B90CA

Next level provided us two more images and another 7z archive, we applied the same logic and we got another password. We started automatizing out script so that it can achive that task alone. At some point our script started failing, we opened the images and without surprise images started to be transformed, mirrored around Y axis, X axis and so on. We implemented more algorithm and restarted the process from that point.

In the end we implemented:

  • mirror Y
  • mirror X
  • mirror X and Y
  • rotate clockwise by 90°
  • rotate counter clockwise by 90°
  • rotate counter clockwise by 90° and mirror Y
  • rotate counter clockwise by 90° and mirror X

The policy we applied to decide wich transformation to apply is that we try the mirror only if the two images have the same size and we apply rotations otherwise. Then we attempt to find the different pixel and print the password only if there is only one different pixel, otherwise we discard that transformation and attempt another one.

So we chained a shell script to extract archive, cleanup the old ones (for memory reason) and call the python algorithm to process images.

After 1024 iterations we got level_1024.txt file with the flag.

Flag:

{FLG:p1xel0ut0fBound3xcept1on_tr4p_1s_shutt1ng_d0wn}

brute.py

import sys
from PIL import Image

if len(sys.argv) < 3:
    print(f'{sys.argv[0]} file.png fil3.png')
    exit()


def makePassword(x, y, r1, g1, b1, r2, g2, b2):
    xy = str(x) + str(y)
    rgb1 = '{:0{}X}'.format(r1, 2) + '{:0{}X}'.format(g1, 2) + '{:0{}X}'.format(b1, 2)
    rgb2 = '{:0{}X}'.format(r2, 2) + '{:0{}X}'.format(g2, 2) + '{:0{}X}'.format(b2, 2)

    return xy + rgb1 + rgb2


img1 = Image.open(sys.argv[1])
img2 = Image.open(sys.argv[2])

width, heigth = img1.size


def findPassword(algorithm):
    passwords = []
    for x in range(width):
        for y in range(heigth):
            rgb1 = img1.getpixel((x, y))
            try:
                rgb2 = img2.getpixel(algorithm(x, y))
            except:
                return None

            if rgb1 != rgb2:
                password = makePassword(x, y, *rgb1, *rgb2)
                passwords.append(password)

        if len(passwords) > 1:
            return None

    return passwords[0]


transformations = []

normal = lambda x,y: (x,y)

# Rotate around x axis
rotateX = lambda x, y: (x, heigth - y - 1)
# Rotate around y axis
rotateY = lambda x, y: (width - x - 1, y)
# Rotate around x and y axis
rotateXY = lambda x,y: (width - x - 1, heigth - y - 1)

# Rotate clockwise 90°
rotateC90 = lambda x,y: (heigth - y - 1, x)
# Rotate counterclockwise 90°
rotateCC90 = lambda x,y: (y, width - x - 1)

# Rotate counterclockwise 90° AND Mirror around X axis
rotateCC90MirrorY = lambda x,y: (heigth - y - 1, width - x - 1)
# Rotate counterclockwise 90° AND Mirror around X axis
rotateCC90MirrorX = lambda x,y: (y, width - (width-x-1) - 1)

# Mirroring
if img1.size == img2.size:
    transformations += [
        normal,
        rotateX,
        rotateY,
        rotateXY,
    ]

# Rotating and more
if img1.size[0] == img2.size[1]:
    transformations += [
        rotateC90,
        rotateCC90,
        rotateCC90MirrorY,
        rotateCC90MirrorX
    ]

for l in transformations:
    pwd = findPassword(l)

    if pwd:
        print(pwd)
        exit(0)

exit(12)

automatize.sh

#/bin/sh

i=0

while true; do
    password=$(python3 brute.py "level_$i.png" "lev3l_$i.png")

    if [ $? -eq 12 ]
    then
        echo "Implement new algorithm"
        exit 1
    fi

    i=$(($i+1))

    echo "$i -> $password"

    7z x "level_$i.7z" -p$password > /dev/null

    if [ $? -eq 0 ]
    then
        rm "level_$i.7z"
    else
        echo "Failure" >&2
        exit 1
    fi

done

Poeta Errante Chronicles

Category: Misc

Points: 100

Solved by: staccah_staccah

Problem

Description: The space-time coordinates bring R-boy to Florence in 1320 AD. Zer0 has just stolen the unpublished version of the "Divine Comedy" from its real author, the "Wandering Poet", giving it to his evil brother, Dante.
Help the "Wandering Poet" recover his opera and let the whole world discover the truth.

Writeup

We were provided with an ip and a port. When we connected to it with netcat we discovered that the challenge was about an old style text adventure in which we were forced to answer some questions in a specific way to keep going on
Main challenge is composed by 3 sub-challenges:

#1 Challenge

It gaves us a weird document composed by hex characters (first_challenge.txt).

When we cleaned the text we tried various conversions, the right one was unicode interpretation.
QR
The result is a QR code composed by unicode characters, translated it there was a basic ID card whose "address" field was the answer to our riddle:
"Where are we going?": Via Vittorio Emanuele II, 21

#2 Challenge

We had to guess a 4 digit code helped by some hints in form of a very known riddle:

4170    1 correct digit and in wrong position
5028    2 correct digits and in right position
7526    1 correct digit and in right position
8350    2 correct digits and in wrong position

Insert the code: 3029

#3 Challenge

Another hexdump was printed (third_challenge.txt), this time it was similar to network packets and the hint in the text:

like sending a big, whole, message, but, instead, dived it in little p...ieces,
and send them one at time...

Confirmed it. So we imported the pcap with text2pcap and opened it in wireshark. There was a communication captured inside it and reading the packet in the right order we got the flag:
{FLG:i7430prrn33743<}

Maze Graph

Category: web

Points: 100

Solved by: drw0if

Problem

Now R-Boy can start his chase. He lands in 1230 BC during the reign of Ramses II. In the Valley of the Temples, Zer0 has plundered Nefertiti’s tomb to resell the precious treasures on the black market. By accident, the guards catch R-Boy near the tomb. To prove he’s not a thief, he has to show his devotion to the Pharaoh by finding a secret note.

Writeup

We are provided with the url gamebox1.reply.it/a37881ac48f4f21d0fb67607d6066ef7/, the corresponding page said that a /graphql url was present on the domain, so we went there.

What we found was a graphiql instance so definitely we must face a graphql challenge.

Graphql is an open-source data query and manipulation language for APIs so it is possible to retrieve and store data in a much easier way then writing and using REST policy.

Thanks to graphiql we can discover easily the requests we can make, so we don't need to find out ourselves. In particular the following query were available:

Request Meaning
allPublicPosts -> [Post] retrieves data from the post marked as public
allUsers -> [User] retrieves data from all the users
me -> User retrieves the current user data
post(id: int) -> Post retrieves data from the speciefied post
user(id: int) -> User retrieves data from the specified user
getAsset(name: String) -> String retrieves a string from the the specified name

There were also some user-defined objects with the following structure:

{
  "name": "RootQuery",
  "kind": "OBJECT",
  "fields": ["me", "allUsers", "user", "post", "allPublicPosts", "getAsset"]
}
{
  "name": "User",
  "kind": "OBJECT",
  "fields": ["id", "username", "firstName", "lastName", "posts"]
}
{
  "name": "Post",
  "kind": "OBJECT",
  "fields": ["id", "title", "content", "public", "author]
}

This could be found using the following __schema query:

{
  __schema {
    types {
      name
      kind
      fields {
        name
      }
    }
  }
}

We firstly looped through all the public posts but nothing important was there. We then queried all the users and for each of them we retrieved their posts:

{
allUsers{
  id
  username
  firstName
  lastName
  posts{
    id
    content
  }
}}

and again nothing important was there.

We then moved on to the post(id) query; we didn't know the id range but it was for sure an incremental numeration. So we decided to enumerate all the posts starting from 1 and for each of them we printed only the private one and only the ones whose content didn't started with "uselesess". With this enumeration we found a post with id = 40 with the content:

{
  "data": {
    "post": {
      "id": "40",
      "title": "Personal notes",
      "content": "Remember to delete the ../mysecretmemofile asset.",
      "public": false
    }
  }
}

We knew we were near the end, so we used the getAsset API:

{
  getAsset(name: "../mysecretmemofile")
}

And we got the flag:

{
  "data": {
    "getAsset": "{FLG:st4rt0ffwith4b4ng!}\n"
  }
}

Tag

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.