text: 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:
  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
  - 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_differencesfunction 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:
  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" } }